Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8263583: Emoji rendering on macOS #3007

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/java.desktop/macosx/native/libawt_lwawt/font/AWTFont.h
Expand Up @@ -41,3 +41,5 @@
+ (NSFont *) nsFontForJavaFont:(jobject)javaFont env:(JNIEnv *)env;

@end

bool IsEmojiFont(CTFontRef font);
11 changes: 11 additions & 0 deletions src/java.desktop/macosx/native/libawt_lwawt/font/AWTFont.m
Expand Up @@ -583,3 +583,14 @@ static OSStatus CreateFSRef(FSRef *myFSRefPtr, NSString *inPath)
CFRelease(fds);
JNI_COCOA_EXIT(env);
}

static CFStringRef EMOJI_FONT_NAME = CFSTR("Apple Color Emoji");

bool IsEmojiFont(CTFontRef font)
{
CFStringRef name = CTFontCopyFullName(font);
if (name == NULL) return false;
bool isFixedColor = CFStringCompare(name, EMOJI_FONT_NAME, 0) == kCFCompareEqualTo;
CFRelease(name);
return isFixedColor;
}
4 changes: 2 additions & 2 deletions src/java.desktop/macosx/native/libawt_lwawt/font/AWTStrike.m
Expand Up @@ -151,7 +151,7 @@ + (AWTStrike *) awtStrikeForFont:(AWTFont *)awtFont
// to indicate we should use CoreText to substitute the character
CGGlyph glyph;
const CTFontRef fallback = CTS_CopyCTFallbackFontAndGlyphForJavaGlyphCode(awtFont, glyphCode, &glyph);
CTFontGetAdvancesForGlyphs(fallback, kCTFontDefaultOrientation, &glyph, &advance, 1);
CGGlyphImages_GetGlyphMetrics(fallback, &awtStrike->fAltTx, awtStrike->fStyle, &glyph, 1, NULL, &advance);
CFRelease(fallback);
advance = CGSizeApplyAffineTransform(advance, awtStrike->fFontTx);
if (!JRSFontStyleUsesFractionalMetrics(awtStrike->fStyle)) {
Expand Down Expand Up @@ -188,7 +188,7 @@ + (AWTStrike *) awtStrikeForFont:(AWTFont *)awtFont
const CTFontRef fallback = CTS_CopyCTFallbackFontAndGlyphForJavaGlyphCode(awtFont, glyphCode, &glyph);

CGRect bbox;
JRSFontGetBoundingBoxesForGlyphsAndStyle(fallback, &tx, awtStrike->fStyle, &glyph, 1, &bbox);
CGGlyphImages_GetGlyphMetrics(fallback, &tx, awtStrike->fStyle, &glyph, 1, &bbox, NULL);
CFRelease(fallback);

// the origin of this bounding box is relative to the bottom-left corner baseline
Expand Down
Expand Up @@ -33,5 +33,12 @@ void
CGGlyphImages_GetGlyphImagePtrs(jlong glyphInfos[],
const AWTStrike *strike,
jint rawGlyphCodes[], const CFIndex len);

void
CGGlyphImages_GetGlyphMetrics(const CTFontRef font,
const CGAffineTransform *tx,
const JRSFontRenderingStyle style,
const CGGlyph glyphs[],
size_t count,
CGRect bboxes[],
CGSize advances[]);
#endif /* __CGGLYPHIMAGES_H */
183 changes: 158 additions & 25 deletions src/java.desktop/macosx/native/libawt_lwawt/font/CGGlyphImages.m
Expand Up @@ -309,6 +309,40 @@ @implementation CGGI_GlyphCanvas
}
}

static void
CGGI_CopyImageFromCanvasToARGBInfo(CGGI_GlyphCanvas *canvas, GlyphInfo *info)
{
CGBitmapInfo bitmapInfo = CGBitmapContextGetBitmapInfo(canvas->context);
bool littleEndian = (bitmapInfo & kCGBitmapByteOrderMask) == kCGBitmapByteOrder32Little;

UInt32 *src = (UInt32 *)canvas->image->data;
size_t srcRowWidth = canvas->image->width;

UInt8 *dest = (UInt8 *)info->image;
size_t destRowWidth = info->width;

size_t height = info->height;

size_t y;

for (y = 0; y < height; y++) {
size_t srcRow = y * srcRowWidth;
if (littleEndian) {
UInt16 destRowBytes = info->rowBytes;
memcpy(dest, src + srcRow, destRowBytes);
dest += destRowBytes;
} else {
size_t x;
for (x = 0; x < destRowWidth; x++) {
UInt32 p = src[srcRow + x];
*dest++ = (p >> 24 & 0xFF); // blue (alpha-premultiplied)
*dest++ = (p >> 16 & 0xFF); // green (alpha-premultiplied)
*dest++ = (p >> 8 & 0xFF); // red (alpha-premultiplied)
*dest++ = (p & 0xFF); // alpha
}
}
}
}

#pragma mark --- Pixel Size, Modes, and Canvas Shaping Helper Functions ---

Expand All @@ -326,6 +360,14 @@ @implementation CGGI_GlyphCanvas
{ 1, &CGGI_CopyImageFromCanvasToAlphaInfo };
static CGGI_GlyphInfoDescriptor rgb =
{ 3, &CGGI_CopyImageFromCanvasToRGBInfo };
static CGGI_GlyphInfoDescriptor argb =
{ 4, &CGGI_CopyImageFromCanvasToARGBInfo };

static inline CGGI_GlyphInfoDescriptor*
CGGI_GetGlyphInfoDescriptor(const CGGI_RenderingMode *mode, CTFontRef font)
{
return IsEmojiFont(font) ? &argb : mode->glyphDescriptor;
}

static inline CGGI_RenderingMode
CGGI_GetRenderingMode(const AWTStrike *strike)
Expand Down Expand Up @@ -459,11 +501,11 @@ @implementation CGGI_GlyphCanvas
}

/*
* Clear the canvas by blitting white only into the region of interest
* Clear the canvas by blitting white (or transparent background for color glyphs) only into the region of interest
* (the rect which we will copy out of once the glyph is struck).
*/
static inline void
CGGI_ClearCanvas(CGGI_GlyphCanvas *canvas, GlyphInfo *info)
CGGI_ClearCanvas(CGGI_GlyphCanvas *canvas, GlyphInfo *info, bool transparent)
{
vImage_Buffer canvasRectToClear;
canvasRectToClear.data = canvas->image->data;
Expand All @@ -474,13 +516,16 @@ @implementation CGGI_GlyphCanvas

// clean the canvas
#ifdef CGGI_DEBUG
Pixel_8888 opaqueWhite = { 0xE0, 0xE0, 0xE0, 0xE0 };
Pixel_8888 background = { 0xE0, 0xE0, 0xE0, 0xE0 };
#else
Pixel_8888 opaqueWhite = { 0xFF, 0xFF, 0xFF, 0xFF };
Pixel_8888 background = { transparent ? 0 : 0xFF,
transparent ? 0 : 0xFF,
transparent ? 0 : 0xFF,
transparent ? 0 : 0xFF };
#endif

// clear canvas background and set foreground color
vImageBufferFill_ARGB8888(&canvasRectToClear, opaqueWhite, kvImageNoFlags);
// clear canvas background
vImageBufferFill_ARGB8888(&canvasRectToClear, background, kvImageNoFlags);
}


Expand All @@ -493,9 +538,9 @@ @implementation CGGI_GlyphCanvas
static inline GlyphInfo *
CGGI_CreateNewGlyphInfoFrom(CGSize advance, CGRect bbox,
const AWTStrike *strike,
const CGGI_RenderingMode *mode)
const CGGI_GlyphInfoDescriptor *glyphDescriptor)
{
size_t pixelSize = mode->glyphDescriptor->pixelSize;
size_t pixelSize = glyphDescriptor->pixelSize;

// adjust the bounding box to be 1px bigger on each side than what
// CGFont-whatever suggests - because it gives a bounding box that
Expand Down Expand Up @@ -560,7 +605,7 @@ @implementation CGGI_GlyphCanvas
static inline void
CGGI_CreateImageForGlyph
(CGGI_GlyphCanvas *canvas, const CGGlyph glyph,
GlyphInfo *info, const CGGI_RenderingMode *mode)
GlyphInfo *info, const CGGI_GlyphInfoDescriptor *glyphDescriptor, const AWTStrike *strike, CTFontRef font)
{
if (isnan(info->topLeftX) || isnan(info->topLeftY)) {
// Explicitly set glyphInfo width/height to be 0 to ensure
Expand All @@ -569,20 +614,48 @@ @implementation CGGI_GlyphCanvas
info->height = 0;

// copy the "empty" glyph from the canvas into the info
(*mode->glyphDescriptor->copyFxnPtr)(canvas, info);
(*glyphDescriptor->copyFxnPtr)(canvas, info);
return;
}

// clean the canvas
CGGI_ClearCanvas(canvas, info);
CGGI_ClearCanvas(canvas, info, glyphDescriptor == &argb);

// strike the glyph in the upper right corner
CGContextShowGlyphsAtPoint(canvas->context,
-info->topLeftX,
canvas->image->height + info->topLeftY,
&glyph, 1);
CGFloat x = -info->topLeftX;
CGFloat y = canvas->image->height + info->topLeftY;

if (glyphDescriptor == &argb) {
// Emoji glyphs are not rendered by CGContextShowGlyphsAtPoint.
// Also, it's not possible to use transformation matrix to get the emoji glyph
// rendered for the desired font size - actual-size font object is needed.
// The logic here must match the logic in CGGlyphImages_GetGlyphMetrics,
// which calculates glyph metrics.

CGAffineTransform matrix = CGContextGetTextMatrix(canvas->context);
CGFloat fontSize = sqrt(fabs(matrix.a * matrix.d - matrix.b * matrix.c));
CTFontRef sizedFont = CTFontCreateCopyWithSymbolicTraits(font, fontSize, NULL, 0, 0);

CGFloat normFactor = 1.0 / fontSize;
CGAffineTransform normalizedMatrix = CGAffineTransformScale(matrix, normFactor, normFactor);
CGContextSetTextMatrix(canvas->context, normalizedMatrix);

CGPoint userPoint = CGPointMake(x, y);
CGAffineTransform normalizedMatrixInv = CGAffineTransformInvert(normalizedMatrix);
CGPoint textPoint = CGPointApplyAffineTransform(userPoint, normalizedMatrixInv);

CTFontDrawGlyphs(sizedFont, &glyph, &textPoint, 1, canvas->context);

CFRelease(sizedFont);
// restore context's original state
CGContextSetTextMatrix(canvas->context, matrix);
CGContextSetFontSize(canvas->context, 1); // CTFontDrawGlyphs tampers with it
} else {
CGContextShowGlyphsAtPoint(canvas->context, x, y, &glyph, 1);
}

// copy the glyph from the canvas into the info
(*mode->glyphDescriptor->copyFxnPtr)(canvas, info);
(*glyphDescriptor->copyFxnPtr)(canvas, info);
}

/*
Expand Down Expand Up @@ -615,13 +688,13 @@ @implementation CGGI_GlyphCanvas
JRSFontRenderingStyle style = JRSFontAlignStyleForFractionalMeasurement(strike->fStyle);

CGRect bbox;
JRSFontGetBoundingBoxesForGlyphsAndStyle(fallback, &tx, style, &glyph, 1, &bbox);

CGSize advance;
CTFontGetAdvancesForGlyphs(fallback, kCTFontDefaultOrientation, &glyph, &advance, 1);
CGGlyphImages_GetGlyphMetrics(fallback, &tx, style, &glyph, 1, &bbox, &advance);

CGGI_GlyphInfoDescriptor *glyphDescriptor = CGGI_GetGlyphInfoDescriptor(mode, fallback);

// create the Sun2D GlyphInfo we are going to strike into
GlyphInfo *info = CGGI_CreateNewGlyphInfoFrom(advance, bbox, strike, mode);
GlyphInfo *info = CGGI_CreateNewGlyphInfoFrom(advance, bbox, strike, glyphDescriptor);

// fix the context size, just in case the substituted character is unexpectedly large
CGGI_SizeCanvas(canvas, info->width, info->height, mode);
Expand All @@ -634,7 +707,7 @@ @implementation CGGI_GlyphCanvas
CFRelease(cgFallback);

// clean the canvas - align, strike, and copy the glyph from the canvas into the info
CGGI_CreateImageForGlyph(canvas, glyph, info, mode);
CGGI_CreateImageForGlyph(canvas, glyph, info, glyphDescriptor, strike, fallback);

// restore the state of the world
CGContextRestoreGState(canvas->context);
Expand Down Expand Up @@ -677,11 +750,14 @@ @implementation CGGI_GlyphCanvas
CGContextSetFont(canvas->context, strike->fAWTFont->fNativeCGFont);
JRSFontSetRenderingStyleOnContext(canvas->context, strike->fStyle);

CTFontRef mainFont = (CTFontRef)strike->fAWTFont->fFont;
CGGI_GlyphInfoDescriptor* mainFontDescriptor = CGGI_GetGlyphInfoDescriptor(mode, mainFont);

CFIndex i;
for (i = 0; i < len; i++) {
GlyphInfo *info = (GlyphInfo *)jlong_to_ptr(glyphInfos[i]);
if (info != NULL) {
CGGI_CreateImageForGlyph(canvas, glyphs[i], info, mode);
CGGI_CreateImageForGlyph(canvas, glyphs[i], info, mainFontDescriptor, strike, mainFont);
} else {
info = CGGI_CreateImageForUnicode(canvas, strike, mode, uniChars[i]);
glyphInfos[i] = ptr_to_jlong(info);
Expand Down Expand Up @@ -774,8 +850,9 @@ @implementation CGGI_GlyphCanvas
CGAffineTransform tx = strike->fTx;
JRSFontRenderingStyle bboxCGMode = JRSFontAlignStyleForFractionalMeasurement(strike->fStyle);

JRSFontGetBoundingBoxesForGlyphsAndStyle((CTFontRef)font->fFont, &tx, bboxCGMode, glyphs, len, bboxes);
CTFontGetAdvancesForGlyphs((CTFontRef)font->fFont, kCTFontDefaultOrientation, glyphs, advances, len);
CTFontRef fontRef = (CTFontRef)font->fFont;
CGGlyphImages_GetGlyphMetrics(fontRef, &tx, bboxCGMode, glyphs, len, bboxes, advances);
CGGI_GlyphInfoDescriptor* mainFontDescriptor = CGGI_GetGlyphInfoDescriptor(mode, fontRef);

size_t maxWidth = 1;
size_t maxHeight = 1;
Expand All @@ -792,7 +869,7 @@ @implementation CGGI_GlyphCanvas
CGSize advance = advances[i];
CGRect bbox = bboxes[i];

GlyphInfo *glyphInfo = CGGI_CreateNewGlyphInfoFrom(advance, bbox, strike, mode);
GlyphInfo *glyphInfo = CGGI_CreateNewGlyphInfoFrom(advance, bbox, strike, mainFontDescriptor);

if (maxWidth < glyphInfo->width) maxWidth = glyphInfo->width;
if (maxHeight < glyphInfo->height) maxHeight = glyphInfo->height;
Expand Down Expand Up @@ -888,3 +965,59 @@ @implementation CGGI_GlyphCanvas

free(buffer);
}

/*
* Calculates bounding boxes (for given transform) and advance (for untransformed 1pt-size font) for specified glyphs.
*/
void
CGGlyphImages_GetGlyphMetrics(const CTFontRef font,
const CGAffineTransform *tx,
const JRSFontRenderingStyle style,
const CGGlyph glyphs[],
size_t count,
CGRect bboxes[],
CGSize advances[]) {
if (IsEmojiFont(font)) {
// Glyph metrics for emoji font are not strictly proportional to font size,
// so we need to construct real-sized font object to calculate them.
// The logic here must match the logic in CGGI_CreateImageForGlyph,
// which performs glyph drawing.

CGFloat fontSize = sqrt(fabs(tx->a * tx->d - tx->b * tx->c));
CTFontRef sizedFont = CTFontCreateCopyWithSymbolicTraits(font, fontSize, NULL, 0, 0);

if (bboxes) {
// JRSFontGetBoundingBoxesForGlyphsAndStyle works incorrectly for AppleColorEmoji font:
// it uses bottom left corner of the glyph's bounding box as a fixed point of transformation
// instead of glyph's origin point (used at drawing). So, as a workaround,
// we request a bounding box for the untransformed glyph, and apply the transform ourselves.
JRSFontGetBoundingBoxesForGlyphsAndStyle(sizedFont, &CGAffineTransformIdentity, style, glyphs, count, bboxes);
CGAffineTransform txNormalized = CGAffineTransformMake(tx->a / fontSize,
tx->b / fontSize,
tx->c / fontSize,
tx->d / fontSize,
0, 0);
for (int i = 0; i < count; i++) {
bboxes[i] = CGRectApplyAffineTransform(bboxes[i], txNormalized);
}
}

if (advances) {
CTFontGetAdvancesForGlyphs(sizedFont, kCTFontDefaultOrientation, glyphs, advances, count);
for (int i = 0; i < count; i++) {
// Calling code will scale the result back
advances[i].width /= fontSize;
advances[i].height /= fontSize;
}
}

CFRelease(sizedFont);
} else {
if (bboxes) {
JRSFontGetBoundingBoxesForGlyphsAndStyle(font, tx, style, glyphs, count, bboxes);
}
if (advances) {
CTFontGetAdvancesForGlyphs(font, kCTFontDefaultOrientation, glyphs, advances, count);
}
}
}