Skip to content

Commit

Permalink
8290866: Apple Color Emoji turns gray after JavaFX version 18
Browse files Browse the repository at this point in the history
Reviewed-by: kcr, angorya
  • Loading branch information
prrace committed Feb 24, 2023
1 parent a916629 commit 0de0837
Show file tree
Hide file tree
Showing 15 changed files with 505 additions and 25 deletions.
Expand Up @@ -38,4 +38,10 @@ public interface CompositeFontResource extends FontResource {
*/
public int getSlotForFont(String fontName);

default boolean isColorGlyph(int glyphCode) {
int slot = (glyphCode >>> 24);
int slotglyphCode = glyphCode & CompositeGlyphMapper.GLYPHMASK;
FontResource slotResource = getSlotResource(slot);
return slotResource.isColorGlyph(slotglyphCode);
}
}
Expand Up @@ -40,6 +40,8 @@ public interface FontConstants {
public static final int nameTag = 0x6E616D65; // 'name'
public static final int os_2Tag = 0x4F532F32; // 'OS/2'
public static final int postTag = 0x706F7374; // 'post'
public static final int colrTag = 0x434F4C52; // 'COLR'
public static final int sbixTag = 0x73626978; // 'sbix'

/* sizes, in bytes, of TT/TTC header records */
public static final int TTCHEADERSIZE = 12;
Expand Down
Expand Up @@ -116,4 +116,7 @@ public FontStrike getStrike(float size, BaseTransform transform,
public void setPeer(Object peer);

public boolean isEmbeddedFont();

public boolean isColorGlyph(int gc);

}
Expand Up @@ -1178,6 +1178,33 @@ public FontStrike getStrike(float size, BaseTransform transform) {
return getStrike(size, transform, getDefaultAAMode());
}

@Override
public float getAdvance(int glyphCode, float ptSize) {
if (glyphCode == CharToGlyphMapper.INVISIBLE_GLYPH_ID) {
return 0f;
}

/*
* Platform-specific but it needs to be explained why this is needed.
* The hmtx table in the Apple Color Emoji font can be woefully off
* compared to the size of emoji glyph CoreText generates and the advance
* CoreText supports. So for macOS at least, we need to get those advances
* another way. Note : I also see "small" discrepancies for ordinary
* glyphs in the mac system font between hmtx and CoreText.
* Limit use of this because we aren't caching the result.
*/
if (PrismFontFactory.isMacOSX && isColorGlyph(glyphCode)) {
return getAdvanceFromPlatform(glyphCode, ptSize);
} else {
return getAdvanceFromHMTX(glyphCode, ptSize);
}
}

/* REMIND: We can cache here if it is slow */
protected float getAdvanceFromPlatform(int glyphCode, float ptSize) {
return getAdvanceFromHMTX(glyphCode, ptSize);
}

char[] advanceWidths = null;
/*
* This is returning the unhinted advance, should be OK so
Expand Down Expand Up @@ -1208,10 +1235,7 @@ public FontStrike getStrike(float size, BaseTransform transform) {
* they do not provide hdmx entry for sizes below that where hinting is
* required, suggesting the htmx table is fine for such cases.
*/
@Override
public float getAdvance(int glyphCode, float ptSize) {
if (glyphCode == CharToGlyphMapper.INVISIBLE_GLYPH_ID)
return 0f;
private float getAdvanceFromHMTX(int glyphCode, float ptSize) {

// If we haven't initialised yet, do so now.
if (advanceWidths == null && numHMetrics > 0) {
Expand Down Expand Up @@ -1374,4 +1398,114 @@ public boolean equals(Object obj) {
public int hashCode() {
return filename.hashCode() + (71 * fullName.hashCode());
}


private boolean checkedColorTables;
private boolean hasColorTables;
private synchronized boolean fontSupportsColorGlyphs() {
if (checkedColorTables) {
return hasColorTables;
}
hasColorTables =
getDirectoryEntry(sbixTag) != null ||
getDirectoryEntry(colrTag) != null;
checkedColorTables = true;

return hasColorTables;
}

public boolean isColorGlyph(int glyphID) {
if (!fontSupportsColorGlyphs()) {
return false;
}
if (getDirectoryEntry(sbixTag) != null) {
return isSbixGlyph(glyphID);
}
return false;
}


private static final int USHORT_MASK = 0xffff;
private static final int UINT_MASK = 0xffffffff;

static class ColorGlyphStrike {

private int ppem;
private int ppi;
private int dataOffsets[];

ColorGlyphStrike(int ppem, int ppi, int[] offsets) {
this.ppem = ppem;
this.ppi = ppi ;
dataOffsets = offsets;
}

boolean hasGlyph(int gid) {
if (gid >= dataOffsets.length-1) {
return false;
}
/* Per the OpenType sbix specthere's one extra offset.
*/
return dataOffsets[gid] < dataOffsets[gid+1];
}
}

ColorGlyphStrike[] sbixStrikes = null;

private boolean isSbixGlyph(int glyphID) {
if (sbixStrikes == null) {
synchronized (this) {
buildSbixStrikeTables();
if (sbixStrikes == null) {
sbixStrikes = new ColorGlyphStrike[0];
}
}
}
for (int i=0; i<sbixStrikes.length; i++) {
if (sbixStrikes[i].hasGlyph(glyphID)) {
return true;
}
}
return false;
}

private void buildSbixStrikeTables() {

Buffer sbixTable = readTable(sbixTag);

if (sbixTable == null) {
return;
}
int sz = sbixTable.capacity();
sbixTable.skip(4); // past version and flags
int numStrikes = sbixTable.getInt() & UINT_MASK;
if (numStrikes <= 0 || numStrikes >= sz) {
return;
}
int[] strikeOffsets = new int[numStrikes];
for (int i=0; i<numStrikes; i++) {
strikeOffsets[i] = sbixTable.getInt() & UINT_MASK;
if (strikeOffsets[i] >= sz) {
return;
}
}
int numGlyphs = getNumGlyphs();
ColorGlyphStrike[] strikes = new ColorGlyphStrike[numStrikes];
for (int i=0; i<numStrikes; i++) {
if (strikeOffsets[i] + 4 + (4*(numGlyphs+1)) > sz) {
return;
}
sbixTable.position(strikeOffsets[i]);

int ppem = sbixTable.getChar() & USHORT_MASK;
int ppi = sbixTable.getChar() & USHORT_MASK;
int[] glyphDataOffsets = new int[numGlyphs+1];
for (int g=0; g<=numGlyphs; g++) {
glyphDataOffsets[g] = sbixTable.getInt() & UINT_MASK;
}
strikes[i] = new ColorGlyphStrike(ppem, ppi, glyphDataOffsets);
}
sbixStrikes = strikes;
}

}
Expand Up @@ -135,7 +135,21 @@ Path2D getGlyphOutline(int gc, float size) {
return path;
}

@Override protected int[] createGlyphBoundingBox(int gc) {
@Override protected float getAdvanceFromPlatform(int glyphCode, float ptSize) {
CTFontStrike strike =
(CTFontStrike)getStrike(ptSize, BaseTransform.IDENTITY_TRANSFORM);
long fontRef = strike.getFontRef();
int orientation = OS.kCTFontOrientationDefault;
CGSize size = new CGSize();
return (float)OS.CTFontGetAdvancesForGlyphs(fontRef, orientation, (short)glyphCode, size);
}

@Override protected int[] createGlyphBoundingBox(int gc) {
/*
* This is being done at size 12 so that the font can cache
* bounds and scale to the required point size. But if the
* bounds do not scale linearly this will fail badly
*/
float size = 12;
CTFontStrike strike = (CTFontStrike)getStrike(size,
BaseTransform.IDENTITY_TRANSFORM);
Expand All @@ -148,13 +162,25 @@ Path2D getGlyphOutline(int gc, float size) {
* The fix is to use the 'loca' and the 'glyf' tables to determine
* the glyph bounding box (same as T2K). This implementation
* uses native code to read these tables since they can be large.
* However for color (emoji) glyphs this returns the wrong bounds,
* so use CTFontGetBoundingRectsForGlyphs anyway.
* In case it fails, or the font doesn't have a glyph table
* (CFF fonts), then the bounds of the glyph outline is used instead.
*/
if (!isCFF()) {
short format = getIndexToLocFormat();
if (OS.CTFontGetBoundingRectForGlyphUsingTables(fontRef, (short)gc, format, bb)) {
if (isColorGlyph(gc)) {
CGRect rect = OS.CTFontGetBoundingRectForGlyphs(fontRef, (short)gc);
float scale = getUnitsPerEm() / size;
bb[0] = (int)(Math.round(rect.origin.x * scale));
bb[1] = (int)(Math.round(rect.origin.y * scale));
bb[2] = (int)(Math.round((rect.origin.x + rect.size.width) * scale));
bb[3] = (int)(Math.round((rect.origin.y + rect.size.height) * scale));
return bb;
} else {
short format = getIndexToLocFormat();
if (OS.CTFontGetBoundingRectForGlyphUsingTables(fontRef, (short)gc, format, bb)) {
return bb;
}
}
}
/* Note: not using tx here as the bounds need to be y up */
Expand Down
Expand Up @@ -86,8 +86,6 @@ private void checkBounds() {
xAdvance = size.width;
yAdvance = -size.height; /*Inverted coordinates system */

if (drawShapes) return;

/* Avoid CTFontGetBoundingRectsForGlyphs as it is too slow */
// bounds = OS.CTFontGetBoundingRectsForGlyphs(fontRef, orientation, (short)glyphCode, null, 1);

Expand Down Expand Up @@ -154,6 +152,41 @@ private long getCachedContext(boolean lcd) {
return cachedContextRef;
}

private synchronized byte[] getColorImage(double x, double y, int w, int h) {

if (w == 0 || h == 0) return new byte[0];

long fontRef = strike.getFontRef();
CGAffineTransform matrix = strike.matrix;
long context = createContext(true, w, h);
if (context == 0) return new byte[0];

double drawX = 0, drawY = 0;
if (matrix != null) {
OS.CGContextTranslateCTM(context, -x, -y);
} else {
drawX = x;
drawY = y;
}

OS.CTFontDrawGlyphs(fontRef, (short)glyphCode, -drawX, -drawY, context);

if (matrix != null) {
OS.CGContextTranslateCTM(context, x, y);
}

byte[] imageData = OS.CGImageContextGetData(context, w, h, 32);
if (imageData == null) {
bounds = new CGRect();
imageData = new byte[0];
}

OS.CGContextRelease(context);

return imageData;
}


private synchronized byte[] getImage(double x, double y, int w, int h, int subPixel) {

if (w == 0 || h == 0) return new byte[0];
Expand Down Expand Up @@ -213,8 +246,14 @@ private synchronized byte[] getImage(double x, double y, int w, int h, int subPi

@Override public byte[] getPixelData(int subPixel) {
checkBounds();
return getImage(bounds.origin.x, bounds.origin.y,
(int)bounds.size.width, (int)bounds.size.height, subPixel);
if (isColorGlyph()) {
return getColorImage(bounds.origin.x, bounds.origin.y,
(int)bounds.size.width, (int)bounds.size.height);
} else {
return getImage(bounds.origin.x, bounds.origin.y,
(int)bounds.size.width, (int)bounds.size.height,
subPixel);
}
}

@Override public float getAdvance() {
Expand All @@ -236,7 +275,11 @@ private synchronized byte[] getImage(double x, double y, int w, int h, int subPi
@Override public int getWidth() {
checkBounds();
int w = (int)bounds.size.width;
return isLCDGlyph() ? w * 3 : w;
if (isColorGlyph()) {
return (w * 4); // has alpha
} else {
return isLCDGlyph() ? w * 3 : w;
}
}

@Override public int getHeight() {
Expand All @@ -256,6 +299,11 @@ private synchronized byte[] getImage(double x, double y, int w, int h, int subPi
return -h - y; /*Inverted coordinates system */
}

public boolean isColorGlyph() {
CTFontFile fontResource = strike.getFontResource();
return fontResource.isColorGlyph(glyphCode);
}

@Override public boolean isLCDGlyph() {
return strike.getAAMode() == FontResource.AA_LCD;
}
Expand Down
Expand Up @@ -59,13 +59,15 @@ static final long CFStringCreate(String string) {

/* Custom */
static final native byte[] CGBitmapContextGetData(long c, int width, int height, int bpp);
static final native byte[] CGImageContextGetData(long c, int width, int height, int bpp);
static final native void CGRectApplyAffineTransform(CGRect rect, CGAffineTransform t);
static final native Path2D CGPathApply(long path);
static final native CGRect CGPathGetPathBoundingBox(long path);
static final native long CFStringCreateWithCharacters(long alloc, char[] chars, long start, long numChars);
static final native String CTFontCopyAttributeDisplayName(long font);
static final native void CTFontDrawGlyphs(long font, short glyphs, double x, double y, long context);
static final native double CTFontGetAdvancesForGlyphs(long font, int orientation, short glyphs, CGSize advances);
static final native CGRect CTFontGetBoundingRectForGlyphs(long font, short glyph);
static final native boolean CTFontGetBoundingRectForGlyphUsingTables(long font, short glyphs, short format, int[] retArr);
static final native int CTRunGetGlyphs(long run, int slotMask, int start, int[] buffer);
static final native int CTRunGetStringIndices(long run, int start, int[] buffer);
Expand Down
Expand Up @@ -290,6 +290,18 @@ private FontStrike getStrike(BaseTransform xform) {
g.setNodeBounds(null);
}

/*
* drawAsShapes() is used for large glyphs to avoid blowing the cache.
* But emojis aren't (currently) cached and may not be available as shapes.
* So the drawAsShapes path results in blank space instead of a large emoji
* This check is used in renderText() where we would otherwise use shapes
* to prevent that.
*/
private boolean isEmojiRun(TextRun run, FontStrike strike) {
FontResource res = strike.getFontResource();
return strike.drawAsShapes() && res.isColorGlyph(run.getGlyphCode(0));
}

private void renderText(Graphics g, FontStrike strike, BaseBounds clipBds,
Color selectionColor, int op) {
for (int i = 0; i < runs.length; i++) {
Expand All @@ -307,7 +319,7 @@ private void renderText(Graphics g, FontStrike strike, BaseBounds clipBds,
y -= lineBounds.getMinY();

if ((op & TEXT) != 0 && run.getGlyphCount() > 0) {
if ((op & FILL) != 0) {
if (((op & FILL) != 0) || isEmojiRun(run, strike)) {
int start = run.getStart();
g.drawString(run, strike, x, y,
selectionColor,
Expand Down

0 comments on commit 0de0837

Please sign in to comment.