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..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 @@ -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 @@ -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) { @@ -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= 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..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 @@ -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..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 @@ -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..1d625811e70 --- /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) 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. + + + """; + + 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(); + } + +}