Skip to content

Commit 0de0837

Browse files
committed
8290866: Apple Color Emoji turns gray after JavaFX version 18
Reviewed-by: kcr, angorya
1 parent a916629 commit 0de0837

File tree

15 files changed

+505
-25
lines changed

15 files changed

+505
-25
lines changed

modules/javafx.graphics/src/main/java/com/sun/javafx/font/CompositeFontResource.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,10 @@ public interface CompositeFontResource extends FontResource {
3838
*/
3939
public int getSlotForFont(String fontName);
4040

41+
default boolean isColorGlyph(int glyphCode) {
42+
int slot = (glyphCode >>> 24);
43+
int slotglyphCode = glyphCode & CompositeGlyphMapper.GLYPHMASK;
44+
FontResource slotResource = getSlotResource(slot);
45+
return slotResource.isColorGlyph(slotglyphCode);
46+
}
4147
}

modules/javafx.graphics/src/main/java/com/sun/javafx/font/FontConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public interface FontConstants {
4040
public static final int nameTag = 0x6E616D65; // 'name'
4141
public static final int os_2Tag = 0x4F532F32; // 'OS/2'
4242
public static final int postTag = 0x706F7374; // 'post'
43+
public static final int colrTag = 0x434F4C52; // 'COLR'
44+
public static final int sbixTag = 0x73626978; // 'sbix'
4345

4446
/* sizes, in bytes, of TT/TTC header records */
4547
public static final int TTCHEADERSIZE = 12;

modules/javafx.graphics/src/main/java/com/sun/javafx/font/FontResource.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,7 @@ public FontStrike getStrike(float size, BaseTransform transform,
116116
public void setPeer(Object peer);
117117

118118
public boolean isEmbeddedFont();
119+
120+
public boolean isColorGlyph(int gc);
121+
119122
}

modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,33 @@ public FontStrike getStrike(float size, BaseTransform transform) {
11781178
return getStrike(size, transform, getDefaultAAMode());
11791179
}
11801180

1181+
@Override
1182+
public float getAdvance(int glyphCode, float ptSize) {
1183+
if (glyphCode == CharToGlyphMapper.INVISIBLE_GLYPH_ID) {
1184+
return 0f;
1185+
}
1186+
1187+
/*
1188+
* Platform-specific but it needs to be explained why this is needed.
1189+
* The hmtx table in the Apple Color Emoji font can be woefully off
1190+
* compared to the size of emoji glyph CoreText generates and the advance
1191+
* CoreText supports. So for macOS at least, we need to get those advances
1192+
* another way. Note : I also see "small" discrepancies for ordinary
1193+
* glyphs in the mac system font between hmtx and CoreText.
1194+
* Limit use of this because we aren't caching the result.
1195+
*/
1196+
if (PrismFontFactory.isMacOSX && isColorGlyph(glyphCode)) {
1197+
return getAdvanceFromPlatform(glyphCode, ptSize);
1198+
} else {
1199+
return getAdvanceFromHMTX(glyphCode, ptSize);
1200+
}
1201+
}
1202+
1203+
/* REMIND: We can cache here if it is slow */
1204+
protected float getAdvanceFromPlatform(int glyphCode, float ptSize) {
1205+
return getAdvanceFromHMTX(glyphCode, ptSize);
1206+
}
1207+
11811208
char[] advanceWidths = null;
11821209
/*
11831210
* This is returning the unhinted advance, should be OK so
@@ -1208,10 +1235,7 @@ public FontStrike getStrike(float size, BaseTransform transform) {
12081235
* they do not provide hdmx entry for sizes below that where hinting is
12091236
* required, suggesting the htmx table is fine for such cases.
12101237
*/
1211-
@Override
1212-
public float getAdvance(int glyphCode, float ptSize) {
1213-
if (glyphCode == CharToGlyphMapper.INVISIBLE_GLYPH_ID)
1214-
return 0f;
1238+
private float getAdvanceFromHMTX(int glyphCode, float ptSize) {
12151239

12161240
// If we haven't initialised yet, do so now.
12171241
if (advanceWidths == null && numHMetrics > 0) {
@@ -1374,4 +1398,114 @@ public boolean equals(Object obj) {
13741398
public int hashCode() {
13751399
return filename.hashCode() + (71 * fullName.hashCode());
13761400
}
1401+
1402+
1403+
private boolean checkedColorTables;
1404+
private boolean hasColorTables;
1405+
private synchronized boolean fontSupportsColorGlyphs() {
1406+
if (checkedColorTables) {
1407+
return hasColorTables;
1408+
}
1409+
hasColorTables =
1410+
getDirectoryEntry(sbixTag) != null ||
1411+
getDirectoryEntry(colrTag) != null;
1412+
checkedColorTables = true;
1413+
1414+
return hasColorTables;
1415+
}
1416+
1417+
public boolean isColorGlyph(int glyphID) {
1418+
if (!fontSupportsColorGlyphs()) {
1419+
return false;
1420+
}
1421+
if (getDirectoryEntry(sbixTag) != null) {
1422+
return isSbixGlyph(glyphID);
1423+
}
1424+
return false;
1425+
}
1426+
1427+
1428+
private static final int USHORT_MASK = 0xffff;
1429+
private static final int UINT_MASK = 0xffffffff;
1430+
1431+
static class ColorGlyphStrike {
1432+
1433+
private int ppem;
1434+
private int ppi;
1435+
private int dataOffsets[];
1436+
1437+
ColorGlyphStrike(int ppem, int ppi, int[] offsets) {
1438+
this.ppem = ppem;
1439+
this.ppi = ppi ;
1440+
dataOffsets = offsets;
1441+
}
1442+
1443+
boolean hasGlyph(int gid) {
1444+
if (gid >= dataOffsets.length-1) {
1445+
return false;
1446+
}
1447+
/* Per the OpenType sbix specthere's one extra offset.
1448+
*/
1449+
return dataOffsets[gid] < dataOffsets[gid+1];
1450+
}
1451+
}
1452+
1453+
ColorGlyphStrike[] sbixStrikes = null;
1454+
1455+
private boolean isSbixGlyph(int glyphID) {
1456+
if (sbixStrikes == null) {
1457+
synchronized (this) {
1458+
buildSbixStrikeTables();
1459+
if (sbixStrikes == null) {
1460+
sbixStrikes = new ColorGlyphStrike[0];
1461+
}
1462+
}
1463+
}
1464+
for (int i=0; i<sbixStrikes.length; i++) {
1465+
if (sbixStrikes[i].hasGlyph(glyphID)) {
1466+
return true;
1467+
}
1468+
}
1469+
return false;
1470+
}
1471+
1472+
private void buildSbixStrikeTables() {
1473+
1474+
Buffer sbixTable = readTable(sbixTag);
1475+
1476+
if (sbixTable == null) {
1477+
return;
1478+
}
1479+
int sz = sbixTable.capacity();
1480+
sbixTable.skip(4); // past version and flags
1481+
int numStrikes = sbixTable.getInt() & UINT_MASK;
1482+
if (numStrikes <= 0 || numStrikes >= sz) {
1483+
return;
1484+
}
1485+
int[] strikeOffsets = new int[numStrikes];
1486+
for (int i=0; i<numStrikes; i++) {
1487+
strikeOffsets[i] = sbixTable.getInt() & UINT_MASK;
1488+
if (strikeOffsets[i] >= sz) {
1489+
return;
1490+
}
1491+
}
1492+
int numGlyphs = getNumGlyphs();
1493+
ColorGlyphStrike[] strikes = new ColorGlyphStrike[numStrikes];
1494+
for (int i=0; i<numStrikes; i++) {
1495+
if (strikeOffsets[i] + 4 + (4*(numGlyphs+1)) > sz) {
1496+
return;
1497+
}
1498+
sbixTable.position(strikeOffsets[i]);
1499+
1500+
int ppem = sbixTable.getChar() & USHORT_MASK;
1501+
int ppi = sbixTable.getChar() & USHORT_MASK;
1502+
int[] glyphDataOffsets = new int[numGlyphs+1];
1503+
for (int g=0; g<=numGlyphs; g++) {
1504+
glyphDataOffsets[g] = sbixTable.getInt() & UINT_MASK;
1505+
}
1506+
strikes[i] = new ColorGlyphStrike(ppem, ppi, glyphDataOffsets);
1507+
}
1508+
sbixStrikes = strikes;
1509+
}
1510+
13771511
}

modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTFontFile.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,21 @@ Path2D getGlyphOutline(int gc, float size) {
135135
return path;
136136
}
137137

138-
@Override protected int[] createGlyphBoundingBox(int gc) {
138+
@Override protected float getAdvanceFromPlatform(int glyphCode, float ptSize) {
139+
CTFontStrike strike =
140+
(CTFontStrike)getStrike(ptSize, BaseTransform.IDENTITY_TRANSFORM);
141+
long fontRef = strike.getFontRef();
142+
int orientation = OS.kCTFontOrientationDefault;
143+
CGSize size = new CGSize();
144+
return (float)OS.CTFontGetAdvancesForGlyphs(fontRef, orientation, (short)glyphCode, size);
145+
}
146+
147+
@Override protected int[] createGlyphBoundingBox(int gc) {
148+
/*
149+
* This is being done at size 12 so that the font can cache
150+
* bounds and scale to the required point size. But if the
151+
* bounds do not scale linearly this will fail badly
152+
*/
139153
float size = 12;
140154
CTFontStrike strike = (CTFontStrike)getStrike(size,
141155
BaseTransform.IDENTITY_TRANSFORM);
@@ -148,13 +162,25 @@ Path2D getGlyphOutline(int gc, float size) {
148162
* The fix is to use the 'loca' and the 'glyf' tables to determine
149163
* the glyph bounding box (same as T2K). This implementation
150164
* uses native code to read these tables since they can be large.
165+
* However for color (emoji) glyphs this returns the wrong bounds,
166+
* so use CTFontGetBoundingRectsForGlyphs anyway.
151167
* In case it fails, or the font doesn't have a glyph table
152168
* (CFF fonts), then the bounds of the glyph outline is used instead.
153169
*/
154170
if (!isCFF()) {
155-
short format = getIndexToLocFormat();
156-
if (OS.CTFontGetBoundingRectForGlyphUsingTables(fontRef, (short)gc, format, bb)) {
171+
if (isColorGlyph(gc)) {
172+
CGRect rect = OS.CTFontGetBoundingRectForGlyphs(fontRef, (short)gc);
173+
float scale = getUnitsPerEm() / size;
174+
bb[0] = (int)(Math.round(rect.origin.x * scale));
175+
bb[1] = (int)(Math.round(rect.origin.y * scale));
176+
bb[2] = (int)(Math.round((rect.origin.x + rect.size.width) * scale));
177+
bb[3] = (int)(Math.round((rect.origin.y + rect.size.height) * scale));
157178
return bb;
179+
} else {
180+
short format = getIndexToLocFormat();
181+
if (OS.CTFontGetBoundingRectForGlyphUsingTables(fontRef, (short)gc, format, bb)) {
182+
return bb;
183+
}
158184
}
159185
}
160186
/* Note: not using tx here as the bounds need to be y up */

modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyph.java

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,6 @@ private void checkBounds() {
8686
xAdvance = size.width;
8787
yAdvance = -size.height; /*Inverted coordinates system */
8888

89-
if (drawShapes) return;
90-
9189
/* Avoid CTFontGetBoundingRectsForGlyphs as it is too slow */
9290
// bounds = OS.CTFontGetBoundingRectsForGlyphs(fontRef, orientation, (short)glyphCode, null, 1);
9391

@@ -154,6 +152,41 @@ private long getCachedContext(boolean lcd) {
154152
return cachedContextRef;
155153
}
156154

155+
private synchronized byte[] getColorImage(double x, double y, int w, int h) {
156+
157+
if (w == 0 || h == 0) return new byte[0];
158+
159+
long fontRef = strike.getFontRef();
160+
CGAffineTransform matrix = strike.matrix;
161+
long context = createContext(true, w, h);
162+
if (context == 0) return new byte[0];
163+
164+
double drawX = 0, drawY = 0;
165+
if (matrix != null) {
166+
OS.CGContextTranslateCTM(context, -x, -y);
167+
} else {
168+
drawX = x;
169+
drawY = y;
170+
}
171+
172+
OS.CTFontDrawGlyphs(fontRef, (short)glyphCode, -drawX, -drawY, context);
173+
174+
if (matrix != null) {
175+
OS.CGContextTranslateCTM(context, x, y);
176+
}
177+
178+
byte[] imageData = OS.CGImageContextGetData(context, w, h, 32);
179+
if (imageData == null) {
180+
bounds = new CGRect();
181+
imageData = new byte[0];
182+
}
183+
184+
OS.CGContextRelease(context);
185+
186+
return imageData;
187+
}
188+
189+
157190
private synchronized byte[] getImage(double x, double y, int w, int h, int subPixel) {
158191

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

214247
@Override public byte[] getPixelData(int subPixel) {
215248
checkBounds();
216-
return getImage(bounds.origin.x, bounds.origin.y,
217-
(int)bounds.size.width, (int)bounds.size.height, subPixel);
249+
if (isColorGlyph()) {
250+
return getColorImage(bounds.origin.x, bounds.origin.y,
251+
(int)bounds.size.width, (int)bounds.size.height);
252+
} else {
253+
return getImage(bounds.origin.x, bounds.origin.y,
254+
(int)bounds.size.width, (int)bounds.size.height,
255+
subPixel);
256+
}
218257
}
219258

220259
@Override public float getAdvance() {
@@ -236,7 +275,11 @@ private synchronized byte[] getImage(double x, double y, int w, int h, int subPi
236275
@Override public int getWidth() {
237276
checkBounds();
238277
int w = (int)bounds.size.width;
239-
return isLCDGlyph() ? w * 3 : w;
278+
if (isColorGlyph()) {
279+
return (w * 4); // has alpha
280+
} else {
281+
return isLCDGlyph() ? w * 3 : w;
282+
}
240283
}
241284

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

302+
public boolean isColorGlyph() {
303+
CTFontFile fontResource = strike.getFontResource();
304+
return fontResource.isColorGlyph(glyphCode);
305+
}
306+
259307
@Override public boolean isLCDGlyph() {
260308
return strike.getAAMode() == FontResource.AA_LCD;
261309
}

modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/OS.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@ static final long CFStringCreate(String string) {
5959

6060
/* Custom */
6161
static final native byte[] CGBitmapContextGetData(long c, int width, int height, int bpp);
62+
static final native byte[] CGImageContextGetData(long c, int width, int height, int bpp);
6263
static final native void CGRectApplyAffineTransform(CGRect rect, CGAffineTransform t);
6364
static final native Path2D CGPathApply(long path);
6465
static final native CGRect CGPathGetPathBoundingBox(long path);
6566
static final native long CFStringCreateWithCharacters(long alloc, char[] chars, long start, long numChars);
6667
static final native String CTFontCopyAttributeDisplayName(long font);
6768
static final native void CTFontDrawGlyphs(long font, short glyphs, double x, double y, long context);
6869
static final native double CTFontGetAdvancesForGlyphs(long font, int orientation, short glyphs, CGSize advances);
70+
static final native CGRect CTFontGetBoundingRectForGlyphs(long font, short glyph);
6971
static final native boolean CTFontGetBoundingRectForGlyphUsingTables(long font, short glyphs, short format, int[] retArr);
7072
static final native int CTRunGetGlyphs(long run, int slotMask, int start, int[] buffer);
7173
static final native int CTRunGetStringIndices(long run, int start, int[] buffer);

modules/javafx.graphics/src/main/java/com/sun/javafx/sg/prism/NGText.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,18 @@ private FontStrike getStrike(BaseTransform xform) {
290290
g.setNodeBounds(null);
291291
}
292292

293+
/*
294+
* drawAsShapes() is used for large glyphs to avoid blowing the cache.
295+
* But emojis aren't (currently) cached and may not be available as shapes.
296+
* So the drawAsShapes path results in blank space instead of a large emoji
297+
* This check is used in renderText() where we would otherwise use shapes
298+
* to prevent that.
299+
*/
300+
private boolean isEmojiRun(TextRun run, FontStrike strike) {
301+
FontResource res = strike.getFontResource();
302+
return strike.drawAsShapes() && res.isColorGlyph(run.getGlyphCode(0));
303+
}
304+
293305
private void renderText(Graphics g, FontStrike strike, BaseBounds clipBds,
294306
Color selectionColor, int op) {
295307
for (int i = 0; i < runs.length; i++) {
@@ -307,7 +319,7 @@ private void renderText(Graphics g, FontStrike strike, BaseBounds clipBds,
307319
y -= lineBounds.getMinY();
308320

309321
if ((op & TEXT) != 0 && run.getGlyphCount() > 0) {
310-
if ((op & FILL) != 0) {
322+
if (((op & FILL) != 0) || isEmojiRun(run, strike)) {
311323
int start = run.getStart();
312324
g.drawString(run, strike, x, y,
313325
selectionColor,

0 commit comments

Comments
 (0)