JBR-7232 Refactor deriveFontWithFeatures & JBRFileDialog JBR API

This commit is contained in:
Nikita Gubarkov
2024-05-30 18:59:18 +02:00
parent c5dbd5d1a3
commit c430c1e0b7
9 changed files with 140 additions and 176 deletions

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2000-2023 JetBrains s.r.o.
* 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.
*/
package com.jetbrains.desktop;
import com.jetbrains.internal.JBRApi;
import java.awt.*;
import java.lang.invoke.MethodHandles;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class FontExtensions {
private interface FontExtension {
FontExtension INSTANCE = (FontExtension) JBRApi.internalServiceBuilder(MethodHandles.lookup())
.withStatic("getFeatures", "getFeatures", "java.awt.Font")
.withStatic("isComplexRendering", "isComplexRendering", "java.awt.Font")
.withStatic("isKerning", "isKerning", "java.awt.Font")
.build();
TreeMap<String, Integer> getFeatures(Font font);
boolean isComplexRendering(Font font);
boolean isKerning(Font font);
}
public static String[] featuresToStringArray(Map<String, Integer> features) {
return features.entrySet().stream().map(feature -> (feature.getKey() + "=" + feature.getValue())).
toArray(String[]::new);
}
public static TreeMap<String, Integer> getFeatures(Font font) {
return FontExtension.INSTANCE.getFeatures(font);
}
public static boolean isComplexRendering(Font font) {
return FontExtension.INSTANCE.isComplexRendering(font);
}
public static boolean isKerning(Font font) {
return FontExtension.INSTANCE.isKerning(font);
}
}

View File

@@ -61,6 +61,7 @@ public class JBRApiModule {
.withStatic("getSubpixelResolution", "getSubpixelResolution", "sun.font.FontUtilities")
.withStatic("deriveFontWithFeatures", "deriveFont", "java.awt.Font")
.withStatic("getAvailableFeatures", "getAvailableFeatures", "java.awt.Font")
.withStatic("getEnabledFeatures", "getEnabledFeatures", "java.awt.Font")
.service("com.jetbrains.FontOpenTypeFeatures")
.withStatic("getAvailableFeatures", "getAvailableFeatures", "java.awt.Font")
.clientProxy("java.awt.Font$Features", "com.jetbrains.FontExtensions$Features")

View File

@@ -6,6 +6,7 @@ import java.lang.annotation.Native;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.awt.*;
import java.util.Objects;
public class JBRFileDialog implements Serializable {
@@ -25,28 +26,19 @@ public class JBRFileDialog implements Serializable {
return (JBRFileDialog) getter.get(dialog);
}
/**
* Whether to select files, directories or both (used when common file dialogs are enabled on Windows, or on macOS)
*/
@Native public static final int SELECT_FILES_HINT = 1, SELECT_DIRECTORIES_HINT = 2;
/**
* Whether to allow creating directories or not (used on macOS)
*/
@Native public static final int CREATE_DIRECTORIES_HINT = 4;
public static final String OPEN_FILE_BUTTON_KEY = "jbrFileDialogOpenFile";
public static final String OPEN_DIRECTORY_BUTTON_KEY = "jbrFileDialogSelectDir";
public static final String ALL_FILES_COMBO_KEY = "jbrFileDialogAllFiles";
public int hints = CREATE_DIRECTORIES_HINT;
/**
* Text for "Open" button (used when common file dialogs are enabled on
* Windows).
*/
public String openButtonText;
/**
* Text for "Select Folder" button (used when common file dialogs are
* enabled on Windows).
*/
public String selectFolderButtonText;
public String allFilesFilterDescription;
public String fileFilterDescription;
public String[] fileFilterExtensions;
public void setHints(int hints) {
this.hints = hints;
@@ -55,9 +47,23 @@ public class JBRFileDialog implements Serializable {
return hints;
}
public void setLocalizationStrings(String openButtonText, String selectFolderButtonText) {
this.openButtonText = openButtonText;
this.selectFolderButtonText = selectFolderButtonText;
public void setLocalizationString(String key, String text) {
Objects.requireNonNull(key);
switch (key) {
case OPEN_FILE_BUTTON_KEY -> openButtonText = text;
case OPEN_DIRECTORY_BUTTON_KEY -> selectFolderButtonText = text;
case ALL_FILES_COMBO_KEY -> allFilesFilterDescription = text;
default -> throw new IllegalArgumentException("unrecognized key: " + key);
}
}
public void setLocalizationStrings(String openButtonText, String selectFolderButtonText) {
setLocalizationString(OPEN_FILE_BUTTON_KEY, openButtonText);
setLocalizationString(OPEN_DIRECTORY_BUTTON_KEY, selectFolderButtonText);
}
public void setFileFilterExtensions(String fileFilterDescription, String[] fileFilterExtensions) {
this.fileFilterDescription = fileFilterDescription;
this.fileFilterExtensions = fileFilterExtensions;
}
}

View File

@@ -48,17 +48,11 @@ import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.CharacterIterator;
import java.util.EventListener;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.*;
import sun.awt.ComponentFactory;
import sun.font.AttributeMap;
import sun.font.AttributeValues;
import sun.font.CompositeFont;
import sun.font.CoreMetrics;
import sun.font.CreatedFontTracker;
import sun.font.Font2D;
@@ -284,6 +278,28 @@ public class Font implements java.io.Serializable
public FontPeer getFontPeer(final Font font) {
return font.getFontPeer();
}
@Override
public String[] getFeatures(Font font) {
int size = font.features == null ? 0 : font.features.length;
String[] fs = new String[size + 3];
if (size > 0) System.arraycopy(font.features, 0, fs, 0, size);
fs[size++] = KERN_FEATURE + "=" + font.getAttributeValues().getKerning();
fs[size++] = LIGA_FEATURE + "=" + font.getAttributeValues().getLigatures();
fs[size] = CALT_FEATURE + "=" + font.getAttributeValues().getLigatures();
return fs;
}
@Override
public boolean isComplexRendering(Font font) {
return (font.values != null && (font.values.getLigatures() != 0 || font.values.getTracking() != 0 ||
font.values.getBaselineTransform() != null)) || font.features != null;
}
@Override
public boolean isKerning(Font font) {
return font.values != null && (font.values.getKerning() != 0);
}
}
static {
@@ -455,10 +471,9 @@ public class Font implements java.io.Serializable
* Ordered map choose intentionally as field's type. It allows to correctly comparing two Font objects
*
* @serial
* @see #getFeatures
* @see #deriveFont(Font, Features)
*/
private TreeMap<String, Integer> features = new TreeMap<String, Integer>();
private String[] features;
/**
* The platform specific font information.
@@ -561,10 +576,6 @@ public class Font implements java.io.Serializable
return font2DHandle.font2D;
}
private boolean anyEnabledFeatures() {
return features.values().stream().anyMatch(x -> x != 0);
}
/**
* Creates a new {@code Font} from the specified name, style and
* point size.
@@ -628,7 +639,7 @@ public class Font implements java.io.Serializable
this.pointSize = size;
}
private Font(String name, int style, float sizePts, TreeMap<String, Integer> features) {
private Font(String name, int style, float sizePts, String[] features) {
this.name = (name != null) ? name : "Default";
this.style = (style & ~0x03) == 0 ? style : 0;
this.size = (int)(sizePts + 0.5);
@@ -639,7 +650,7 @@ public class Font implements java.io.Serializable
/* This constructor is used by deriveFont when attributes is null */
private Font(String name, int style, float sizePts,
boolean created, boolean withFallback,
Font2DHandle handle, boolean useOldHandle, TreeMap<String, Integer> features) {
Font2DHandle handle, boolean useOldHandle, String[] features) {
this(name, style, sizePts, features);
this.createdFont = created;
this.withFallback = withFallback;
@@ -704,7 +715,7 @@ public class Font implements java.io.Serializable
*/
private Font(AttributeValues values, String oldName, int oldStyle,
boolean created, boolean withFallback,
Font2DHandle handle, boolean useOldHandle, TreeMap<String, Integer> features) {
Font2DHandle handle, boolean useOldHandle, String[] features) {
this.features = features;
this.createdFont = created;
@@ -768,6 +779,11 @@ public class Font implements java.io.Serializable
* @since 1.6
*/
protected Font(Font font) {
this(font, font.features);
}
private Font(Font font, String[] features) {
this.features = features;
if (font.values != null) {
initFromValues(font.getAttributeValues().clone());
} else {
@@ -779,12 +795,6 @@ public class Font implements java.io.Serializable
this.font2DHandle = font.font2DHandle;
this.createdFont = font.createdFont;
this.withFallback = font.withFallback;
this.features = font.features;
}
private Font(Font font, TreeMap<String, Integer> features) {
this(font);
this.features = features;
}
/**
@@ -834,7 +844,7 @@ public class Font implements java.io.Serializable
if (values.getPosture() >= .2f) this.style |= ITALIC; // not == .2f
this.nonIdentityTx = values.anyNonDefault(EXTRA_MASK);
this.hasLayoutAttributes = values.anyNonDefault(LAYOUT_MASK);
this.hasLayoutAttributes = this.features != null || values.anyNonDefault(LAYOUT_MASK);
}
/**
@@ -915,7 +925,7 @@ public class Font implements java.io.Serializable
values.merge(attributes, SECONDARY_MASK);
return new Font(values, font.name, font.style,
font.createdFont, font.withFallback,
font.font2DHandle, false, new TreeMap<String, Integer>());
font.font2DHandle, false, null);
}
return new Font(attributes);
}
@@ -927,7 +937,7 @@ public class Font implements java.io.Serializable
values.merge(attributes, SECONDARY_MASK);
return new Font(values, font.name, font.style,
font.createdFont, font.withFallback,
font.font2DHandle, false, new TreeMap<String, Integer>());
font.font2DHandle, false, null);
}
return font;
@@ -1648,17 +1658,7 @@ public class Font implements java.io.Serializable
* @since 1.6
*/
public boolean hasLayoutAttributes() {
return anyEnabledFeatures() || hasLayoutAttributes;
}
private static TreeMap<String, Integer> getFeatures(Font font) {
TreeMap<String, Integer> res = new TreeMap<>();
res.putAll(font.features);
res.put(KERN_FEATURE, font.getAttributeValues().getKerning());
res.put(LIGA_FEATURE, font.getAttributeValues().getLigatures());
res.put(CALT_FEATURE, font.getAttributeValues().getLigatures());
return res;
return hasLayoutAttributes;
}
/**
@@ -1866,7 +1866,7 @@ public class Font implements java.io.Serializable
*/
public int hashCode() {
if (hash == 0) {
hash = name.hashCode() ^ style ^ size ^ features.hashCode();
hash = name.hashCode() ^ style ^ size ^ Arrays.hashCode(features);
/* It is possible many fonts differ only in transform.
* So include the transform in the hash calculation.
* nonIdentityTx is set whenever there is a transform in
@@ -1905,7 +1905,7 @@ public class Font implements java.io.Serializable
pointSize == font.pointSize &&
withFallback == font.withFallback &&
name.equals(font.name) &&
features.equals(font.features)) {
Arrays.equals(features, font.features)) {
/* 'values' is usually initialized lazily, except when
* the font is constructed from a Map, or derived using
@@ -2029,17 +2029,13 @@ public class Font implements java.io.Serializable
}
values = getAttributeValues().merge(extras);
this.nonIdentityTx = values.anyNonDefault(EXTRA_MASK);
this.hasLayoutAttributes = values.anyNonDefault(LAYOUT_MASK);
this.hasLayoutAttributes = this.features != null || values.anyNonDefault(LAYOUT_MASK);
} catch (Throwable t) {
throw new IOException(t);
} finally {
fRequestedAttributes = null; // don't need it any more
}
}
if (features == null) {
features = new TreeMap<>();
}
}
/**
@@ -2270,7 +2266,48 @@ public class Font implements java.io.Serializable
}
private static Font deriveFont(Font font, Features features) {
return new Font(font, features.getAsTreeMap());
TreeMap<String, Integer> map = features.getAsTreeMap();
String[] array = new String[map.size()];
int i = 0;
for (Map.Entry<String, Integer> e : map.entrySet()) {
validateFeature(array[i++] = e.getKey() + "=" + e.getValue());
}
return new Font(font, array);
}
private static void validateFeature(String f) {
int len = f.length();
invalid:if ((len == 4 || len > 5) &&
Character.isLetterOrDigit(f.charAt(0)) &&
Character.isLetterOrDigit(f.charAt(1)) &&
Character.isLetterOrDigit(f.charAt(2)) &&
Character.isLetterOrDigit(f.charAt(3))) {
if (len > 5 && f.charAt(4) == '=') {
for (int i = 5; i < len; i++) {
if (!Character.isDigit(f.charAt(i))) break invalid;
}
if (f.startsWith("kern")) throw new IllegalArgumentException(
"\"kern\" feature is not allowed here, use java.awt.font.TextAttribute.KERNING instead");
if (f.startsWith("liga") || f.startsWith("calt")) throw new IllegalArgumentException(
"\"liga\" and \"calt\" features are not allowed here, use java.awt.font.TextAttribute.LIGATURES instead");
return;
}
}
throw new IllegalArgumentException("Invalid feature: \"" + f + "\", allowed syntax: \"kern\", or \"aalt=2\"");
}
private static Font deriveFont(Font font, String... features) {
if (features == null || features.length == 0) return new Font(font, null);
for (String f : features) validateFeature(f);
return new Font(font, Arrays.copyOf(features, features.length));
}
/**
* Returns a list of OpenType's features enabled on the current Font.
* @return array of enabled OpenType's features
*/
private static String[] getEnabledFeatures(Font font) {
return font.features == null ? new String[0] : Arrays.copyOf(font.features, font.features.length);
}
/**
@@ -2724,19 +2761,10 @@ public class Font implements java.io.Serializable
return metrics.charsBounds(chars, beginIndex, limit - beginIndex);
}
private static boolean isComplexRendering(Font font) {
return (font.values != null && (font.values.getLigatures() != 0 || font.values.getTracking() != 0 ||
font.values.getBaselineTransform() != null)) || font.anyEnabledFeatures();
}
private static boolean isKerning(Font font) {
return font.values != null && (font.values.getKerning() != 0);
}
/**
* Returns a list of OpenType's features supported by current Font.
* Implementation of such logic goes to HarfBuzz library.
* @return list of OpenType's features concatenated to String
* @return set of available OpenType's features
*/
private static Set<String> getAvailableFeatures(Font font) {
return SunLayoutEngine.getAvailableFeatures(FontUtilities.getFont2D(font));

View File

@@ -27,6 +27,7 @@ package sun.font;
import java.awt.Font;
import java.awt.peer.FontPeer;
import java.util.TreeMap;
public abstract class FontAccess {
@@ -47,4 +48,7 @@ public abstract class FontAccess {
public abstract void setWithFallback(Font f);
public abstract boolean isCreatedFont(Font f);
public abstract FontPeer getFontPeer(Font f);
public abstract String[] getFeatures(Font font);
public abstract boolean isComplexRendering(Font font);
public abstract boolean isKerning(Font font);
}

View File

@@ -43,7 +43,6 @@ import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import com.jetbrains.desktop.FontExtensions;
import sun.java2d.Disposer;
import sun.java2d.DisposerRecord;
@@ -481,7 +480,7 @@ public final class FontDesignMetrics extends FontMetrics {
assert (data instanceof String || data instanceof char[]);
float width = 0;
if (overrider == null && FontExtensions.isComplexRendering(font) && len > 0) {
if (overrider == null && FontAccess.getFontAccess().isComplexRendering(font) && len > 0) {
return textLayoutBounds(data, off, len);
}
@@ -492,7 +491,7 @@ public final class FontDesignMetrics extends FontMetrics {
return new Rectangle2D.Float(0f, -ascent, width, height);
}
boolean isKerning = FontExtensions.isKerning(font);
boolean isKerning = FontAccess.getFontAccess().isKerning(font);
float consecutiveDoubleCharacterWidth = 0f;
char prev = 0;
for (int i = off; i < off + len; i++) {
@@ -608,7 +607,7 @@ public final class FontDesignMetrics extends FontMetrics {
return height;
}
private static class Accessor { // used by JBR API
private static final class Accessor {
// Keeping metrics instances here prevents them from being garbage collected
// and being re-created by FontDesignMetrics.getMetrics method
private final Set<FontDesignMetrics> PINNED_METRICS = new HashSet<>();
@@ -647,7 +646,7 @@ public final class FontDesignMetrics extends FontMetrics {
}
}
private interface Overrider { // used by JBR API
private interface Overrider {
float charWidth(int codePoint);
}
}

View File

@@ -68,8 +68,6 @@
package sun.font;
import com.jetbrains.desktop.FontExtensions;
import java.lang.ref.SoftReference;
import java.awt.Font;
import java.awt.font.FontRenderContext;
@@ -176,7 +174,7 @@ public final class GlyphLayout {
* leave pt and the gvdata unchanged.
*/
public void layout(FontStrikeDesc sd, float[] mat, float ptSize, int gmask, int baseIndex, TextRecord text,
boolean ltrDirection, Map<String, Integer> features, Point2D.Float pt, GVData data);
boolean ltrDirection, String[] features, Point2D.Float pt, GVData data);
}
/**
@@ -452,7 +450,7 @@ public final class GlyphLayout {
EngineRecord er = _erecords.get(ix);
for (;;) {
try {
er.layout(ltrDirection, FontExtensions.getFeatures(font));
er.layout(ltrDirection, FontAccess.getFontAccess().getFeatures(font));
break;
}
catch (IndexOutOfBoundsException e) {
@@ -658,7 +656,7 @@ public final class GlyphLayout {
this.engine = _lef.getEngine(key); // flags?
}
void layout(boolean ltrDirection, Map<String, Integer> features) {
void layout(boolean ltrDirection, String[] features) {
_textRecord.start = start;
_textRecord.limit = limit;
engine.layout(_sd, _mat, ptSize, gmask, start - _offset, _textRecord,

View File

@@ -30,7 +30,6 @@
package sun.font;
import com.jetbrains.desktop.FontExtensions;
import sun.font.GlyphLayout.*;
import sun.java2d.Disposer;
import sun.java2d.DisposerRecord;
@@ -174,7 +173,7 @@ public final class SunLayoutEngine implements LayoutEngine, LayoutEngineFactory
}
public void layout(FontStrikeDesc desc, float[] mat, float ptSize, int gmask,
int baseIndex, TextRecord tr, boolean ltrDirection, Map<String, Integer> features,
int baseIndex, TextRecord tr, boolean ltrDirection, String[] features,
Point2D.Float pt, GVData data) {
Font2D font = key.font();
FontStrike strike = font.getStrike(desc);
@@ -183,7 +182,7 @@ public final class SunLayoutEngine implements LayoutEngine, LayoutEngineFactory
shape(font, strike, ptSize, mat, pFace,
tr.text, data, key.script(),
tr.start, tr.limit, baseIndex, pt,
ltrDirection, FontExtensions.featuresToStringArray(features), gmask);
ltrDirection, features, gmask);
}
}

View File

@@ -42,9 +42,6 @@ import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import static com.jetbrains.desktop.FontExtensions.featuresToStringArray;
import static com.jetbrains.desktop.FontExtensions.getFeatures;
public class FontExtensionsTest {
@Retention(RetentionPolicy.RUNTIME)
private @interface JBRTest {}
@@ -96,10 +93,6 @@ public class FontExtensionsTest {
return isImageEquals(image1, image2);
}
private static String[] fontFeaturesAsString(Font font) {
return featuresToStringArray(getFeatures(font));
}
@JBRTest
private static Boolean testFeatureFromString() {
return FontExtensions.FeatureTag.getFeatureTag("frac").isPresent() &&
@@ -108,23 +101,24 @@ public class FontExtensionsTest {
FontExtensions.FeatureTag.getFeatureTag("tttt").isEmpty();
}
@JBRTest
private static Boolean testFeaturesToString1() {
Font font = JBR.getFontExtensions().deriveFontWithFeatures(BASE_FONT, new FontExtensions.Features(Map.of(
FontExtensions.FeatureTag.ZERO, FontExtensions.FEATURE_ON,
FontExtensions.FeatureTag.SALT, 123,
FontExtensions.FeatureTag.FRAC, FontExtensions.FEATURE_OFF)));
String[] features = {"calt=0", "frac=0", "kern=0", "liga=0", "salt=123", "zero=1"};
return Arrays.equals(fontFeaturesAsString(font), features);
}
@JBRTest
private static Boolean testFeaturesToString2() {
Font font = BASE_FONT.deriveFont(Map.of(TextAttribute.LIGATURES, TextAttribute.LIGATURES_ON,
TextAttribute.KERNING, TextAttribute.KERNING_ON));
String[] features = {"calt=1", "kern=1", "liga=1"};
return Arrays.equals(fontFeaturesAsString(font), features);
}
// TODO uncomment these tests when API for retrieving enabled font features is availbale
// @JBRTest
// private static Boolean testFeaturesToString1() {
// Font font = JBR.getFontExtensions().deriveFontWithFeatures(BASE_FONT, new FontExtensions.Features(Map.of(
// FontExtensions.FeatureTag.ZERO, FontExtensions.FEATURE_ON,
// FontExtensions.FeatureTag.SALT, 123,
// FontExtensions.FeatureTag.FRAC, FontExtensions.FEATURE_OFF)));
// String[] features = {"calt=0", "frac=0", "kern=0", "liga=0", "salt=123", "zero=1"};
// return Arrays.equals(fontFeaturesAsString(font), features);
// }
//
// @JBRTest
// private static Boolean testFeaturesToString2() {
// Font font = BASE_FONT.deriveFont(Map.of(TextAttribute.LIGATURES, TextAttribute.LIGATURES_ON,
// TextAttribute.KERNING, TextAttribute.KERNING_ON));
// String[] features = {"calt=1", "kern=1", "liga=1"};
// return Arrays.equals(fontFeaturesAsString(font), features);
// }
@JBRTest
private static Boolean testDisablingLigatureByDefault() {
@@ -257,7 +251,7 @@ public class FontExtensionsTest {
}
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("JBR: internal error during testing");
throw new RuntimeException("JBR: internal error during testing", e);
}
if (!error.isEmpty()) {