Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 130 additions & 6 deletions Frameworks/CoreGraphics/CGContext.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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, ...) \
Expand Down Expand Up @@ -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;
Copy link
Contributor

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.

Copy link
Author

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.)

Copy link
Author

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(!))

if (rect) {
CGRect transformedClippingRegion = CGContextConvertRectToDeviceSpace(this, *rect);

Expand Down Expand Up @@ -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();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state [](start = 10, length = 5)

move it closer to use

Copy link
Contributor

Choose a reason for hiding this comment

The 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++

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would damage clarity: it could be __CGContextDrawingState& or __CGContext::__CGContextDrawingState&. Having it on the right hand side is clearer (in my opinion)

Copy link
Contributor

Choose a reason for hiding this comment

The 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;
Copy link
Contributor

@aballway aballway Dec 13, 2016

Choose a reason for hiding this comment

The 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();
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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:
Copy link
Member

@MSFTFox MSFTFox Dec 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FillGeometry [](start = 11, length = 12)

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. It can also live here for posterity.


In reply to: 92093543 [](ancestors = 92093543)

// * 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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

@aballway aballway Dec 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move a comptr? I love it! #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

The 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));
}

/**
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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());

Expand Down
106 changes: 106 additions & 0 deletions Frameworks/CoreGraphics/CGImage.mm
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
#import "CGImageInternal.h"
#import "CGIWICBitmap.h"

#import <algorithm>

using namespace Microsoft::WRL;

static const wchar_t* TAG = L"CGImage";
Expand Down Expand Up @@ -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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

w = 0, h [](start = 17, length = 8)

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;
Copy link
Contributor

@aballway aballway Dec 13, 2016

Choose a reason for hiding this comment

The 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

Copy link
Author

Choose a reason for hiding this comment

The 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
2 changes: 2 additions & 0 deletions Frameworks/include/CGImageInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is _CGImageCovertMaskToCompatibleWICBitmap more appropriate name?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That'd work!

Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@
<ClangCompile Include="$(StarboardBasePath)\tests\unittests\CoreGraphics.Drawing\CGContextDrawingTests.cpp" />
<ClangCompile Include="$(StarboardBasePath)\tests\unittests\CoreGraphics.Drawing\CGContextDrawing_FillTests.cpp" />
<ClangCompile Include="$(StarboardBasePath)\tests\unittests\CoreGraphics.Drawing\CGContextDrawing_ShadowTests.cpp" />
<ClangCompile Include="$(StarboardBasePath)\tests\unittests\CoreGraphics.Drawing\CGContextDrawing_ImageMaskTests.cpp" />
<ClangCompile Include="$(StarboardBasePath)\tests\unittests\CoreGraphics.Drawing\CGPathDrawingTests.cpp" />
<ClangCompile Include="$(StarboardBasePath)\tests\unittests\CoreGraphics.Drawing\DrawingTest.cpp" />
<ClangCompile Include="$(StarboardBasePath)\tests\unittests\CoreGraphics.drawing\ImageComparison.cpp" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ DISABLED_DRAW_TEST_F(CGContext, DrawAContextIntoAnImage, UIKitMimicTest) {
CGContextDrawImage(context, bounds, image.get());
}


DISABLED_DRAW_TEST_F(CGContext, FillThenStrokeIsSameAsDrawFillStroke, WhiteBackgroundTest) {
CGContextRef context = GetDrawingContext();
CGRect bounds = GetDrawingBounds();
Expand Down
Loading