com.sun.javafx.text.GlyphLayout Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2012, 2024, 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.
*/
/*
* GlyphLayout is used to process a run of text into a run of run of
* glyphs, optionally with position and char mapping info.
*
* The text has already been processed for numeric shaping and bidi.
* The run of text that layout works on has a single bidi level. It
* also has a single font/style. Some operations need context to work
* on (shaping, script resolution) so context for the text run text is
* provided. It is assumed that the text array contains sufficient
* context, and the offset and count delimit the portion of the text
* that needs to actually be processed.
*
* The font might be a composite font. Layout generally requires
* tables from a single physical font to operate, and so it must
* resolve the 'single' font run into runs of physical fonts.
*
* Some characters are supported by several fonts of a composite, and
* in order to properly emulate the glyph substitution behavior of a
* single physical font, these characters might need to be mapped to
* different physical fonts. The script code that is assigned
* characters normally considered 'common script' can be used to
* resolve which physical font to use for these characters. The input
* to the char to glyph mapper (which assigns physical fonts as it
* processes the glyphs) should include the script code, and the
* mapper should operate on runs of a single script.
*
* To perform layout, call get() to get a new (or reuse an old)
* GlyphLayout, call layout on it, then call done(GlyphLayout) when
* finished. There's no particular problem if you don't call done,
* but it assists in reuse of the GlyphLayout.
*/
package com.sun.javafx.text;
import static com.sun.javafx.scene.text.TextLayout.FLAGS_ANALYSIS_VALID;
import static com.sun.javafx.scene.text.TextLayout.FLAGS_HAS_BIDI;
import static com.sun.javafx.scene.text.TextLayout.FLAGS_HAS_CJK;
import static com.sun.javafx.scene.text.TextLayout.FLAGS_HAS_COMPLEX;
import static com.sun.javafx.scene.text.TextLayout.FLAGS_HAS_EMBEDDED;
import static com.sun.javafx.scene.text.TextLayout.FLAGS_HAS_TABS;
import static com.sun.javafx.scene.text.TextLayout.FLAGS_RTL_BASE;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.Bidi;
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.scene.text.TextSpan;
public abstract class GlyphLayout {
public static final int CANONICAL_SUBSTITUTION = 1 << 30;
/**
* A flag bit indicating text direction as determined by Bidi analysis.
*/
public static final int LAYOUT_LEFT_TO_RIGHT = 1 << 0;
public static final int LAYOUT_RIGHT_TO_LEFT = 1 << 1;
/**
* A flag bit indicating that text in the char array
* before the indicated start should not be examined.
*/
public static final int LAYOUT_NO_START_CONTEXT = 1 << 2;
/**
* A flag bit indicating that text in the char array
* after the indicated limit should not be examined.
*/
public static final int LAYOUT_NO_LIMIT_CONTEXT = 1 << 3;
public static final int HINTING = 1 << 4;
/**
* Android versions that still run a dalvik based on JDK 6 (API level 18 and
* before) don't have the method Character.isIdeographic.
* On devices with a JVM that does not have Character.isIdeographic, there will
* be non-optimal line breaking for CJKV.
* The reflection-based approach should be removed in a later version,
* when the Android base version moves to API level 19.
*/
private static Method isIdeographicMethod = null;
static {
try {
isIdeographicMethod = Character.class.getMethod("isIdeographic", int.class);
} catch (NoSuchMethodException | SecurityException e) {
isIdeographicMethod = null;
}
}
protected TextRun addTextRun(PrismTextLayout layout, char[] chars,
int start, int length,
PGFont font, TextSpan span, byte level) {
/* subclass can overwrite this method in order to handle complex text */
TextRun run = new TextRun(start, length, level, true, 0, span, 0, false);
layout.addTextRun(run);
return run;
}
private TextRun addTextRun(PrismTextLayout layout, char[] chars,
int start, int length, PGFont font,
TextSpan span, byte level, boolean complex) {
/* The complex flag indicates complex script, and in general all
* bidi scripts are consider complex. That said, using directional
* control (RLO) is possible to force RTL direction on non-complex
* scripts. Thus, odd level must be threat as complex.
*/
if (complex || (level & 1) != 0) {
return addTextRun(layout, chars, start, length, font, span, level);
}
TextRun run = new TextRun(start, length, level, false, 0, span, 0, false);
layout.addTextRun(run);
return run;
}
public int breakRuns(PrismTextLayout layout, char[] chars, int flags) {
int length = chars.length;
boolean complex = false;
boolean feature = false;
int scriptRun = ScriptMapper.COMMON;
int script = ScriptMapper.COMMON;
boolean checkComplex = true;
boolean checkBidi = true;
if ((flags & FLAGS_ANALYSIS_VALID) != 0) {
/* Avoid work when it is known neither complex
* text nor bidi are not present. */
checkComplex = (flags & FLAGS_HAS_COMPLEX) != 0;
checkBidi = (flags & FLAGS_HAS_BIDI) != 0;
}
TextRun run = null;
Bidi bidi = null;
byte bidiLevel = 0;
int bidiEnd = length;
int bidiIndex = 0;
int spanIndex = 0;
TextSpan span = null;
int spanEnd = length;
PGFont font = null;
TextSpan[] spans = layout.getTextSpans();
if (spans != null) {
if (spans.length > 0) {
span = spans[spanIndex];
spanEnd = span.getText().length();
font = (PGFont)span.getFont();
if (font == null) {
flags |= FLAGS_HAS_EMBEDDED;
}
}
} else {
font = layout.getFont();
}
if (font != null) {
FontResource fr = font.getFontResource();
int requestedFeatures = font.getFeatures();
int supportedFeatures = fr.getFeatures();
feature = (requestedFeatures & supportedFeatures) != 0;
}
if (checkBidi && length > 0) {
int direction = layout.getDirection();
bidi = new Bidi(chars, 0, null, 0, length, direction);
/* Temporary Code: See RT-26997 */
// bidiLevel = (byte)bidi.getRunLevel(bidiIndex);
bidiLevel = (byte)bidi.getLevelAt(bidi.getRunStart(bidiIndex));
bidiEnd = bidi.getRunLimit(bidiIndex);
if ((bidiLevel & 1) != 0) {
flags |= FLAGS_HAS_BIDI | FLAGS_HAS_COMPLEX;
}
}
int start = 0;
int i = 0;
while (i < length) {
char ch = chars[i];
int codePoint = ch;
boolean delimiter = ch == '\t' || ch == '\n' || ch == '\r';
int surrogate = 0;
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 - surrogate;
}
}
boolean spanChanged = i >= spanEnd && i < length;
boolean levelChanged = i >= bidiEnd && i < length;
boolean scriptChanged = false;
if (!delimiter && !isEmoji) {
boolean oldComplex = complex;
if (checkComplex) {
if (isIdeographic(codePoint)) {
flags |= FLAGS_HAS_CJK;
}
/* Check for script changes */
script = ScriptMapper.getScript(codePoint);
if (scriptRun > ScriptMapper.INHERITED &&
script > ScriptMapper.INHERITED &&
script != scriptRun) {
scriptChanged = true;
}
if (!complex) {
complex = feature || ScriptMapper.isComplexCharCode(codePoint);
}
}
if (spanChanged || levelChanged || scriptChanged) {
if (start != i) {
/* Create text run */
run = addTextRun(layout, chars, start, i - start,
font, span, bidiLevel, oldComplex);
if (complex) {
flags |= FLAGS_HAS_COMPLEX;
complex = false;
}
start = i;
}
}
i++;
}
if (spanChanged) {
/* Only true for rich text (spans != null) */
span = spans[++spanIndex];
spanEnd += span.getText().length();
font = (PGFont)span.getFont();
if (font == null) {
flags |= FLAGS_HAS_EMBEDDED;
} else {
FontResource fr = font.getFontResource();
int requestedFeatures = font.getFeatures();
int supportedFeatures = fr.getFeatures();
feature = (requestedFeatures & supportedFeatures) != 0;
}
}
if (levelChanged) {
bidiIndex++;
/* Temporary Code: See RT-26997 */
// bidiLevel = (byte)bidi.getRunLevel(bidiIndex);
bidiLevel = (byte)bidi.getLevelAt(bidi.getRunStart(bidiIndex));
bidiEnd = bidi.getRunLimit(bidiIndex);
if ((bidiLevel & 1) != 0) {
flags |= FLAGS_HAS_BIDI | FLAGS_HAS_COMPLEX;
}
}
if (scriptChanged) {
scriptRun = script;
}
if (delimiter) {
i++;
/* Only merge \r\n when the are in the same text span */
if (ch == '\r' && i < spanEnd && chars[i] == '\n') {
i++;
}
/* Create delimiter run */
run = new TextRun(start, i - start, bidiLevel, false,
ScriptMapper.COMMON, span, 0, false);
if (ch == '\t') {
run.setTab();
flags |= FLAGS_HAS_TABS;
} else {
run.setLinebreak();
}
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 */
if (start < length) {
addTextRun(layout, chars, start, length - start,
font, span, bidiLevel, complex);
if (complex) {
flags |= FLAGS_HAS_COMPLEX;
}
} else {
/* Ensure every lines has at least one run */
if (run == null || run.isLinebreak()) {
run = new TextRun(start, 0, (byte)0, false,
ScriptMapper.COMMON, span, 0, false);
layout.addTextRun(run);
}
}
if (bidi != null) {
if (!bidi.baseIsLeftToRight()) {
flags |= FLAGS_RTL_BASE;
}
}
flags |= FLAGS_ANALYSIS_VALID;
return flags;
}
public abstract void layout(TextRun run, PGFont font,
FontStrike strike, char[] text);
protected int getInitialSlot(FontResource fr) {
/* For some reason, DirectWrite and CoreText do not work with the JRE
* fonts (Lucida Sans). For example, with Arabic text the glyphs
* do not have ligatures (as if all glyphs were generated using just
* the CMAP table). Possible reasons for this failure is the
* presence of a system version of Lucida Sans, which does not include
* Arabic, and that causes some internal cache to fail (since both fonts
* would have the same postscript name); or possibly the JRE fonts
* have some internal settings that causes DirectWrite and
* CoreText to fail. Pango and ICU do not present the same problem.
* The fix is to use a different font.
* This fix relies that a CompositeFontResource has at least one
* fallback, and that is not a JRE font, and this method is used
* only to process complex text.
*/
if (PrismFontFactory.isJreFont(fr)) {
if (PrismFontFactory.debugFonts) {
System.err.println("Avoiding JRE Font: " + fr.getFullName());
}
return 1;
}
return 0;
}
/* This scheme creates a singleton GlyphLayout which is checked out
* for use. Callers who find its checked out create one that after use
* is discarded. This means that in a MT-rendering environment,
* there's no need to synchronise except for that one instance.
* Fewer threads will then need to synchronise, perhaps helping
* throughput on a MP system. If for some reason the reusable
* GlyphLayout is checked out for a long time (or never returned?) then
* we would end up always creating new ones. That situation should not
* occur and if if did, it would just lead to some extra garbage being
* created.
*/
private static GlyphLayout reusableGL = newInstance();
private static boolean inUse;
private static GlyphLayout newInstance() {
PrismFontFactory factory = PrismFontFactory.getFontFactory();
return factory.createGlyphLayout();
}
public static GlyphLayout getInstance() {
/* The following heuristic is that if the reusable instance is
* in use, it probably still will be in a micro-second, so avoid
* synchronising on the class and just allocate a new instance.
* The cost is one extra boolean test for the normal case, and some
* small number of cases where we allocate an extra object when
* in fact the reusable one would be freed very soon.
*/
if (inUse) {
return newInstance();
} else {
synchronized(GlyphLayout.class) {
if (inUse) {
return newInstance();
} else {
inUse = true;
return reusableGL;
}
}
}
}
public void dispose() {
if (this == reusableGL) {
inUse = false;
}
}
private static boolean isIdeographic(int codePoint) {
if (isIdeographicMethod != null) {
try {
return (boolean) isIdeographicMethod.invoke(null, codePoint);
} catch (IllegalAccessException | InvocationTargetException ex) {
return false;
}
}
return false;
}
}