From e5072c9ffea3cc0a594dd00bc6dcb79502d745a6 Mon Sep 17 00:00:00 2001 From: prrace Date: Thu, 23 Feb 2023 12:28:53 -0800 Subject: [PATCH 1/5] 8290866 --- .../javafx/font/CompositeFontResource.java | 6 + .../com/sun/javafx/font/FontConstants.java | 2 + .../com/sun/javafx/font/FontResource.java | 3 + .../com/sun/javafx/font/PrismFontFile.java | 137 +++++++++++++++++- .../sun/javafx/font/coretext/CTFontFile.java | 32 +++- .../com/sun/javafx/font/coretext/CTGlyph.java | 58 +++++++- .../java/com/sun/javafx/font/coretext/OS.java | 2 + .../java/com/sun/javafx/sg/prism/NGText.java | 14 +- .../java/com/sun/javafx/text/GlyphLayout.java | 54 +++++-- .../src/main/java/com/sun/prism/Graphics.java | 37 +++++ .../sun/prism/impl/ps/BaseShaderGraphics.java | 4 + .../com/sun/prism/j2d/J2DPrismGraphics.java | 5 + .../java/com/sun/prism/sw/SWGraphics.java | 6 + .../src/main/native-font/coretext.c | 58 ++++++++ tests/manual/text/EmojiTest.java | 107 ++++++++++++++ 15 files changed, 502 insertions(+), 23 deletions(-) create mode 100644 tests/manual/text/EmojiTest.java diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/CompositeFontResource.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/CompositeFontResource.java index 8f66778552d..f25b23c610d 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/CompositeFontResource.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/CompositeFontResource.java @@ -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); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/FontConstants.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/FontConstants.java index b98cf9eff03..c592ff12b78 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/FontConstants.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/FontConstants.java @@ -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; diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/FontResource.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/FontResource.java index 98561fa2d4b..2a1bf104d73 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/FontResource.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/FontResource.java @@ -116,4 +116,7 @@ public FontStrike getStrike(float size, BaseTransform transform, public void setPeer(Object peer); public boolean isEmbeddedFont(); + + public boolean isColorGlyph(int gc); + } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java index c11963e2548..ff8be207f69 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java @@ -1178,6 +1178,32 @@ public FontStrike getStrike(float size, BaseTransform transform) { return getStrike(size, transform, getDefaultAAMode()); } + + 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 @@ -1208,8 +1234,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) { + private float getAdvanceFromHMTX(int glyphCode, float ptSize) { if (glyphCode == CharToGlyphMapper.INVISIBLE_GLYPH_ID) return 0f; @@ -1374,4 +1399,112 @@ 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 int USHORT_MASK = 0xffff; + private static 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) { + return false; + } + 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= sz) { + return; + } + int[] strikeOffsets = new int[numStrikes]; + for (int i=0; i= sz) { + return; + } + } + int numGlyphs = getNumGlyphs(); + ColorGlyphStrike[] strikes = new ColorGlyphStrike[numStrikes]; + for (int i=0; i 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; + } + } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTFontFile.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTFontFile.java index f1e68359bed..02180ec53f5 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTFontFile.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTFontFile.java @@ -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); @@ -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 */ diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyph.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyph.java index b778d47f4ce..ac01e959cab 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyph.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyph.java @@ -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); @@ -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]; @@ -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() { @@ -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() { @@ -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; } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/OS.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/OS.java index 24b724b7240..0c422b1fb9a 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/OS.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/OS.java @@ -59,6 +59,7 @@ 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); @@ -66,6 +67,7 @@ static final long CFStringCreate(String string) { 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); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/sg/prism/NGText.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/sg/prism/NGText.java index 8ec6115aad7..ee160dc57c9 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/sg/prism/NGText.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/sg/prism/NGText.java @@ -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++) { @@ -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, diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java index 588fc973609..0444be095b5 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java @@ -66,10 +66,12 @@ import java.text.Bidi; +import com.sun.javafx.font.CharToGlyphMapper; import com.sun.javafx.font.FontResource; import com.sun.javafx.font.FontStrike; import com.sun.javafx.font.PGFont; import com.sun.javafx.font.PrismFontFactory; +import com.sun.javafx.font.PrismFontFile; import com.sun.javafx.scene.text.TextSpan; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -203,31 +205,51 @@ public int breakRuns(PrismTextLayout layout, char[] chars, int flags) { char ch = chars[i]; int codePoint = ch; boolean delimiter = ch == '\t' || ch == '\n' || ch == '\r'; + int surrogate = 0; - /* special handling for delimiters */ - if (delimiter) { - if (i != start) { - run = addTextRun(layout, chars, start, i - start, + if (Character.isHighSurrogate(ch)) { + /* Only merge surrogate when the pair is in the same span. */ + if (i + 1 < spanEnd && Character.isLowSurrogate(chars[i + 1])) { + codePoint = Character.toCodePoint(ch, chars[++i]); + surrogate = 1; + } + } + /* + * Since Emojis are usually used one at a time, handle them + * similarly to delimiters - if we have any chars in the current run, + * break the run there. Then (see code later in the method) create + * a new run just for the one emoji and then start the next run. + * Having it in a separate run allows rendering code to more + * efficiently handle it rather than having to switch rendering + * modes in the middle of a drawString. + */ + boolean isEmoji = false; + if (font != null) { + FontResource fr = font.getFontResource(); + int glyphID = fr.getGlyphMapper().charToGlyph(codePoint); + isEmoji = fr.isColorGlyph(glyphID); + } + + /* special handling for delimiters and Emoji */ + if (delimiter || isEmoji) { + if ((i - surrogate) != start) { + run = addTextRun(layout, chars, start, i - surrogate - start, font, span, bidiLevel, complex); if (complex) { flags |= FLAGS_HAS_COMPLEX; complex = false; } - start = i; + start = i - surrogate; } } + boolean spanChanged = i >= spanEnd && i < length; boolean levelChanged = i >= bidiEnd && i < length; boolean scriptChanged = false; - if (!delimiter) { + + if (!delimiter && !isEmoji) { boolean oldComplex = complex; if (checkComplex) { - if (Character.isHighSurrogate(ch)) { - /* Only merge surrogate when the pair is in the same span. */ - if (i + 1 < spanEnd && Character.isLowSurrogate(chars[i + 1])) { - codePoint = Character.toCodePoint(ch, chars[++i]); - } - } if (isIdeographic(codePoint)) { flags |= FLAGS_HAS_CJK; @@ -305,6 +327,14 @@ public int breakRuns(PrismTextLayout layout, char[] chars, int flags) { layout.addTextRun(run); start = i; } + if (isEmoji) { + i++; + /* Create Emoji run */ + run = new TextRun(start, i - start, bidiLevel, false, + ScriptMapper.COMMON, span, 0, false); + layout.addTextRun(run); + start = i; + } } /* Create final text run */ diff --git a/modules/javafx.graphics/src/main/java/com/sun/prism/Graphics.java b/modules/javafx.graphics/src/main/java/com/sun/prism/Graphics.java index e79fe999b56..243cfb98739 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/prism/Graphics.java +++ b/modules/javafx.graphics/src/main/java/com/sun/prism/Graphics.java @@ -25,8 +25,10 @@ package com.sun.prism; +import java.nio.ByteBuffer; import com.sun.glass.ui.Screen; import com.sun.javafx.font.FontStrike; +import com.sun.javafx.font.Glyph; import com.sun.javafx.geom.RectBounds; import com.sun.javafx.geom.Rectangle; import com.sun.javafx.geom.Shape; @@ -36,6 +38,7 @@ import com.sun.javafx.sg.prism.NGCamera; import com.sun.javafx.sg.prism.NGLightBase; import com.sun.javafx.sg.prism.NodePath; +import com.sun.prism.Texture.WrapMode; import com.sun.prism.paint.Color; import com.sun.prism.paint.Paint; @@ -215,4 +218,38 @@ public void drawMappedTextureRaw(Texture tex, public void setPixelScaleFactors(float pixelScaleX, float pixelScaleY); public float getPixelScaleFactorX(); public float getPixelScaleFactorY(); + + + /* + * Identical in each of BaseShaderGraphics, J2DPrismGraphics and SWGraphics + */ + default void drawColorGlyph(GlyphList gl, FontStrike strike, float x, float y, + Color selectColor, int selectStart, int selectEnd) { + + int glyphCode = gl.getGlyphCode(0); + Glyph glyph = strike.getGlyph(glyphCode); + if (glyph == null) { + return; + } + + byte[] glyphImage = glyph.getPixelData(0); + int ox = glyph.getOriginX(); + int oy = glyph.getOriginY(); + int height = glyph.getHeight(); + int bytesWidth = glyph.getWidth(); + // glyph width is reported in bytes, so code here needs to know + // that the bytes-per-pixel is grey=1, lcd=3, color=4 + int width = bytesWidth / 4; + + PixelFormat format = PixelFormat.BYTE_BGRA_PRE; + Texture tex = getResourceFactory().createTexture( + format, Texture.Usage.STATIC, WrapMode.CLAMP_NOT_NEEDED, width, height); + ByteBuffer bb = ByteBuffer.wrap(glyphImage); + int scan = width * tex.getPixelFormat().getBytesPerPixelUnit(); + // format arg is the Buffer pixel format. Texture may not be created with the + // same as requested (ie the software pipeline). + tex.update(bb, format, 0, 0, 0, 0, width, height, scan, false); + drawTexture(tex, x+ox, y+oy, x+ox+width, y+oy+height, 0, 0, width, height); + tex.dispose(); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/prism/impl/ps/BaseShaderGraphics.java b/modules/javafx.graphics/src/main/java/com/sun/prism/impl/ps/BaseShaderGraphics.java index e805b70f722..d11d7b509cf 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/prism/impl/ps/BaseShaderGraphics.java +++ b/modules/javafx.graphics/src/main/java/com/sun/prism/impl/ps/BaseShaderGraphics.java @@ -1985,6 +1985,10 @@ private void initLCDSampleRT() { public void drawString(GlyphList gl, FontStrike strike, float x, float y, Color selectColor, int selectStart, int selectEnd) { + if (strike.getFontResource().isColorGlyph(gl.getGlyphCode(0))) { + drawColorGlyph(gl, strike, x, y, selectColor, selectStart, selectEnd); + return; + } if (isComplexPaint || paint.getType().isImagePattern() || strike.drawAsShapes()) diff --git a/modules/javafx.graphics/src/main/java/com/sun/prism/j2d/J2DPrismGraphics.java b/modules/javafx.graphics/src/main/java/com/sun/prism/j2d/J2DPrismGraphics.java index 47a4877b448..7d18b812c27 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/prism/j2d/J2DPrismGraphics.java +++ b/modules/javafx.graphics/src/main/java/com/sun/prism/j2d/J2DPrismGraphics.java @@ -875,6 +875,11 @@ public void drawString(GlyphList gl, FontStrike strike, float x, float y, int count = gl.getGlyphCount(); if (count == 0) return; + if (strike.getFontResource().isColorGlyph(gl.getGlyphCode(0))) { + drawColorGlyph(gl, strike, x, y, selectColor, start, end); + return; + } + // In JDK6, setting graphics AA disables fast text loops g2d.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF); diff --git a/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWGraphics.java b/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWGraphics.java index 8475d34a645..29f5317e1b0 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWGraphics.java +++ b/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWGraphics.java @@ -58,6 +58,7 @@ import com.sun.prism.ReadbackGraphics; import com.sun.prism.RenderTarget; import com.sun.prism.Texture; +import com.sun.prism.Texture.WrapMode; import com.sun.prism.impl.PrismSettings; import com.sun.prism.paint.Color; import com.sun.prism.paint.ImagePattern; @@ -632,6 +633,11 @@ public void drawString(GlyphList gl, FontStrike strike, float x, float y, ", selectStart: " + selectStart + ", selectEnd: " + selectEnd); } + if (strike.getFontResource().isColorGlyph(gl.getGlyphCode(0))) { + drawColorGlyph(gl, strike, x, y, selectColor, selectStart, selectEnd); + return; + } + final float bx, by, bw, bh; if (paint.isProportional()) { if (nodeBounds != null) { diff --git a/modules/javafx.graphics/src/main/native-font/coretext.c b/modules/javafx.graphics/src/main/native-font/coretext.c index 4a05ffc80d9..2eb1705426d 100644 --- a/modules/javafx.graphics/src/main/native-font/coretext.c +++ b/modules/javafx.graphics/src/main/native-font/coretext.c @@ -768,6 +768,55 @@ JNIEXPORT jstring JNICALL OS_NATIVE(CTFontCopyAttributeDisplayName) return (*env)->NewString(env, (jchar *)buffer, length); } +JNIEXPORT jbyteArray JNICALL OS_NATIVE(CGImageContextGetData) + (JNIEnv *env, jclass that, jlong arg0, jint dstWidth, jint dstHeight, jint bpp) +{ + jbyteArray result = NULL; + if (dstWidth < 0) return NULL; + if (dstHeight < 0) return NULL; + if (bpp != 32) return NULL; + + CGContextRef context = (CGContextRef)arg0; + if (context == NULL) return NULL; + jbyte *srcData = (jbyte*)CGBitmapContextGetData(context); + + if (srcData) { + size_t srcWidth = CGBitmapContextGetWidth(context); + if (srcWidth < dstWidth) return NULL; + size_t srcHeight = CGBitmapContextGetHeight(context); + if (srcHeight < dstHeight) return NULL; + size_t srcBytesPerRow = CGBitmapContextGetBytesPerRow(context); + size_t srcStep = CGBitmapContextGetBitsPerPixel(context) / 8; + int srcOffset = (srcHeight - dstHeight) * srcBytesPerRow; + + int dstStep = bpp / 8; + size_t size = dstWidth * dstHeight * dstStep; + jbyte* data = (jbyte*)calloc(size, sizeof(jbyte)); + if (data == NULL) return NULL; + + int x, y, sx; + + int dstOffset = 0; + for (y = 0; y < dstHeight; y++) { + for (x = 0, sx = 0; x < dstWidth; x++, dstOffset += dstStep, sx += srcStep) { + /* BGRA to BGRA */ + data[dstOffset + 0] = srcData[srcOffset + sx + 0]; + data[dstOffset + 1] = srcData[srcOffset + sx + 1]; + data[dstOffset + 2] = srcData[srcOffset + sx + 2]; + data[dstOffset + 3] = srcData[srcOffset + sx + 3]; + } + srcOffset += srcBytesPerRow; + } + + result = (*env)->NewByteArray(env, size); + if (result) { + (*env)->SetByteArrayRegion(env, result, 0, size, data); + } + free(data); + } + return result; +} + JNIEXPORT jbyteArray JNICALL OS_NATIVE(CGBitmapContextGetData) (JNIEnv *env, jclass that, jlong arg0, jint dstWidth, jint dstHeight, jint bpp) { @@ -845,6 +894,15 @@ JNIEXPORT void JNICALL OS_NATIVE(CTFontDrawGlyphs) CTFontDrawGlyphs((CTFontRef)arg0, glyphs, pos, 1, (CGContextRef)contextRef); } +JNIEXPORT jobject JNICALL OS_NATIVE(CTFontGetBoundingRectForGlyphs) + (JNIEnv *env, jclass that, jlong arg1, jshort arg2) +{ + CTFontRef fontRef = (CTFontRef)arg1; + CGGlyph glyphs[] = {arg2}; + CGRect bb = CTFontGetBoundingRectsForGlyphs(fontRef, (CTFontOrientation)0, glyphs, NULL, 1); + return newCGRect(env, &bb); +} + JNIEXPORT jboolean JNICALL OS_NATIVE(CTFontGetBoundingRectForGlyphUsingTables) (JNIEnv *env, jclass that, jlong arg1, jshort arg2, jshort arg3, jintArray arg4) { diff --git a/tests/manual/text/EmojiTest.java b/tests/manual/text/EmojiTest.java new file mode 100644 index 00000000000..41485bbcda7 --- /dev/null +++ b/tests/manual/text/EmojiTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import javafx.scene.text.Font; +import javafx.scene.text.Text; + +public class EmojiTest extends Application { + + static String instructions = + """ + This tests rendering of Emoji glyphs which is only supported on macOS. + On macOS you should see a yellow-coloured smiling face image, + embedded between 'ab' and 'cd'. + On other platforms it may be a missing glyph, or an empty space, or + a similar rendering as a greyscale/B&W glyph. + Principally, you are checking that the emoji is rendered on macOS in + each of the controls and nodes displayed in the test, and that the + editable text field handles selection of the emoji glyph with the + same background as other glyphs - this presumes the emoji image has + transparent background pixels. + There are 3 different ways it is displayed to verify + 1) Text node. 2) Label control, 3) TextFile Control + Press the Pass or Fail button as appropriate and the test will exit. + If what you see is not explained here, ask before filing a bug. + + + """; + + public static void main(String[] args) { + launch(args); + } + + private void quit() { + Platform.exit(); + } + + @Override + public void start(Stage stage) { + Button passButton = new Button("Pass"); + Button failButton = new Button("Fail"); + passButton.setOnAction(e -> this.quit()); + failButton.setOnAction(e -> { + this.quit(); + throw new AssertionError("The Emoji was not rendered on macOS"); + }); + + HBox hbox = new HBox(10, passButton, failButton); + + Text instTA = new Text(instructions); + instTA.setWrappingWidth(500); + + Font font = new Font(32); + String emojiString = "ab\ud83d\ude00cd"; + Text text = new Text(emojiString); + text.setFont(font); + Label label = new Label(emojiString); + label.setFont(font); + TextField textField = new TextField(emojiString); + textField.setFont(font); + + VBox vbox = new VBox(); + Scene scene = new Scene(vbox); + vbox.getChildren().add(instTA); + vbox.getChildren().add(hbox); + vbox.getChildren().add(text); + vbox.getChildren().add(label); + vbox.getChildren().add(textField); + stage.setWidth(600); + stage.setHeight(600); + stage.setScene(scene); + + stage.show(); + } + +} From 2416aeb66fbbaa5492fc6ae63f52a0e6e35e0024 Mon Sep 17 00:00:00 2001 From: prrace Date: Thu, 23 Feb 2023 12:43:08 -0800 Subject: [PATCH 2/5] 8290866 --- .../com/sun/javafx/font/PrismFontFile.java | 20 ++++----- .../src/main/java/com/sun/prism/Graphics.java | 2 +- tests/manual/text/EmojiTest.java | 42 +++++++++---------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java index ff8be207f69..f1746eca6b3 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java @@ -1178,7 +1178,7 @@ public FontStrike getStrike(float size, BaseTransform transform) { return getStrike(size, transform, getDefaultAAMode()); } - + public float getAdvance(int glyphCode, float ptSize) { if (glyphCode == CharToGlyphMapper.INVISIBLE_GLYPH_ID) return 0f; @@ -1400,7 +1400,7 @@ public int hashCode() { return filename.hashCode() + (71 * fullName.hashCode()); } - + private boolean checkedColorTables; private boolean hasColorTables; private synchronized boolean fontSupportsColorGlyphs() { @@ -1425,7 +1425,7 @@ public boolean isColorGlyph(int glyphID) { return false; } - + private static int USHORT_MASK = 0xffff; private static int UINT_MASK = 0xffffffff; @@ -1440,7 +1440,7 @@ static class ColorGlyphStrike { this.ppi = ppi ; dataOffsets = offsets; } - + boolean hasGlyph(int gid) { if (gid >= dataOffsets.length) { return false; @@ -1464,14 +1464,14 @@ private boolean isSbixGlyph(int glyphID) { if (sbixStrikes[i].hasGlyph(glyphID)) { return true; } - } + } return false; } private void buildSbixStrikeTables() { Buffer sbixTable = readTable(sbixTag); - + if (sbixTable == null) { return; } @@ -1481,7 +1481,7 @@ private void buildSbixStrikeTables() { if (numStrikes <= 0 || numStrikes >= sz) { return; } - int[] strikeOffsets = new int[numStrikes]; + int[] strikeOffsets = new int[numStrikes]; for (int i=0; i= sz) { @@ -1498,13 +1498,13 @@ private void buildSbixStrikeTables() { int ppem = sbixTable.getChar() & USHORT_MASK; int ppi = sbixTable.getChar() & USHORT_MASK; - int[] glyphDataOffsets = new int[numGlyphs+1]; + 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; + sbixStrikes = strikes; } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/prism/Graphics.java b/modules/javafx.graphics/src/main/java/com/sun/prism/Graphics.java index 243cfb98739..9fdf5457509 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/prism/Graphics.java +++ b/modules/javafx.graphics/src/main/java/com/sun/prism/Graphics.java @@ -220,7 +220,7 @@ public void drawMappedTextureRaw(Texture tex, public float getPixelScaleFactorY(); - /* + /* * Identical in each of BaseShaderGraphics, J2DPrismGraphics and SWGraphics */ default void drawColorGlyph(GlyphList gl, FontStrike strike, float x, float y, diff --git a/tests/manual/text/EmojiTest.java b/tests/manual/text/EmojiTest.java index 41485bbcda7..091ebabce9f 100644 --- a/tests/manual/text/EmojiTest.java +++ b/tests/manual/text/EmojiTest.java @@ -23,20 +23,20 @@ * questions. */ -import javafx.application.Application; -import javafx.application.Platform; +import javafx.application.Application; +import javafx.application.Platform; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; -import javafx.scene.Group; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.TextField; -import javafx.stage.Stage; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.stage.Stage; import javafx.scene.text.Font; import javafx.scene.text.Text; -public class EmojiTest extends Application { +public class EmojiTest extends Application { static String instructions = """ @@ -58,16 +58,16 @@ public class EmojiTest extends Application { """; - public static void main(String[] args) { - launch(args); - } + public static void main(String[] args) { + launch(args); + } private void quit() { Platform.exit(); } - @Override - public void start(Stage stage) { + @Override + public void start(Stage stage) { Button passButton = new Button("Pass"); Button failButton = new Button("Fail"); passButton.setOnAction(e -> this.quit()); @@ -77,7 +77,7 @@ public void start(Stage stage) { }); HBox hbox = new HBox(10, passButton, failButton); - + Text instTA = new Text(instructions); instTA.setWrappingWidth(500); @@ -97,11 +97,11 @@ public void start(Stage stage) { vbox.getChildren().add(text); vbox.getChildren().add(label); vbox.getChildren().add(textField); - stage.setWidth(600); - stage.setHeight(600); - stage.setScene(scene); + stage.setWidth(600); + stage.setHeight(600); + stage.setScene(scene); - stage.show(); - } + stage.show(); + } -} +} From acff6dab11392b7fa30fec27136f3ea20ea9c4a1 Mon Sep 17 00:00:00 2001 From: prrace Date: Fri, 24 Feb 2023 12:27:03 -0800 Subject: [PATCH 3/5] 8290866 --- .../java/com/sun/javafx/font/PrismFontFile.java | 14 ++++++++------ .../java/com/sun/javafx/font/coretext/CTGlyph.java | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java index f1746eca6b3..866eb124012 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java @@ -1178,10 +1178,11 @@ 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) + if (glyphCode == CharToGlyphMapper.INVISIBLE_GLYPH_ID) { return 0f; + } /* * Platform-specific but it needs to be explained why this is needed. @@ -1235,8 +1236,6 @@ protected float getAdvanceFromPlatform(int glyphCode, float ptSize) { * required, suggesting the htmx table is fine for such cases. */ private float getAdvanceFromHMTX(int glyphCode, float ptSize) { - if (glyphCode == CharToGlyphMapper.INVISIBLE_GLYPH_ID) - return 0f; // If we haven't initialised yet, do so now. if (advanceWidths == null && numHMetrics > 0) { @@ -1426,8 +1425,8 @@ public boolean isColorGlyph(int glyphID) { } - private static int USHORT_MASK = 0xffff; - private static int UINT_MASK = 0xffffffff; + private static final int USHORT_MASK = 0xffff; + private static final int UINT_MASK = 0xffffffff; static class ColorGlyphStrike { @@ -1445,6 +1444,9 @@ boolean hasGlyph(int gid) { if (gid >= dataOffsets.length) { return false; } + /* The following is correct, per the OpenType sbix spec + * there's one extra offset. + */ return dataOffsets[gid] < dataOffsets[gid+1]; } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyph.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyph.java index ac01e959cab..360aaa8c365 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyph.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyph.java @@ -278,7 +278,7 @@ private synchronized byte[] getImage(double x, double y, int w, int h, int subPi if (isColorGlyph()) { return (w * 4); // has alpha } else { - return isLCDGlyph() ? w * 3 : w; + return isLCDGlyph() ? w * 3 : w; } } From cae5274cb4446cfba435bcd2cbc901f9244f54b6 Mon Sep 17 00:00:00 2001 From: prrace Date: Fri, 24 Feb 2023 13:16:04 -0800 Subject: [PATCH 4/5] 8290866 --- .../src/main/java/com/sun/javafx/font/PrismFontFile.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java index 866eb124012..ce79cf72cce 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/PrismFontFile.java @@ -1441,11 +1441,10 @@ static class ColorGlyphStrike { } boolean hasGlyph(int gid) { - if (gid >= dataOffsets.length) { + if (gid >= dataOffsets.length-1) { return false; } - /* The following is correct, per the OpenType sbix spec - * there's one extra offset. + /* Per the OpenType sbix specthere's one extra offset. */ return dataOffsets[gid] < dataOffsets[gid+1]; } From 82bf8d334de22e6f11d7ba4a1288c211b8d9013a Mon Sep 17 00:00:00 2001 From: prrace Date: Fri, 24 Feb 2023 14:20:07 -0800 Subject: [PATCH 5/5] 8290866 --- tests/manual/text/EmojiTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/manual/text/EmojiTest.java b/tests/manual/text/EmojiTest.java index 091ebabce9f..1d625811e70 100644 --- a/tests/manual/text/EmojiTest.java +++ b/tests/manual/text/EmojiTest.java @@ -51,7 +51,7 @@ public class EmojiTest extends Application { same background as other glyphs - this presumes the emoji image has transparent background pixels. There are 3 different ways it is displayed to verify - 1) Text node. 2) Label control, 3) TextFile Control + 1) Text node. 2) Label control, 3) TextField Control Press the Pass or Fail button as appropriate and the test will exit. If what you see is not explained here, ask before filing a bug.