Ширина строки при вычислении шрифтов очень медленная, если в тексте есть арабские или персидские буквы

У меня проблема. Мой интерфейс приложения работает намного медленнее, если я использую восточные языки там. Особенно я ощущал это в таких компонентах, как JList, JCombobox, JTable.

Как я нашел, что производительность метода FontMetrics.stringWidth очень медленная (500+ раз), если в тексте хотя бы одна буква является арабской или персидской. Как я знаю, что это обычно используемый метод в различных компонентах свинг.

Есть ли способ повысить производительность этого метода?

Вот пример класса, который демонстрирует проблему:

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.image.BufferedImage;

public class FontMetricsSpeedTest
{

 public static void main( String args[] ) {
  String persian="صصصصصصصصصصصصصصصصصصصصص";
  String english="abcde()agjklj;lkjelwk";
  FontMetrics fm=createFontMetrics(new Font("dialog",Font.PLAIN,12));
  int size=50000;
  long start=System.currentTimeMillis();
  for(int i=0;i<size;i++)
  {
   fm.stringWidth(persian);
  }
  System.out.println("Calculation time for persian: "+(System.currentTimeMillis()-start)+" ms");
  start=System.currentTimeMillis();
  for(int i=0;i<size;i++)
  {
   fm.stringWidth(english);
  }
  System.out.println("Calculation time for english: "+(System.currentTimeMillis()-start)+" ms");
 }
 private static FontMetrics createFontMetrics(Font font)
 {
  BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB_PRE);
  Graphics g = bi.getGraphics();
  FontMetrics fm = g.getFontMetrics(font);
  g.dispose();
  bi = null;
  return fm;
 }
}

Для меня это дает следующий вывод:

Время вычисления для персидского языка: 5482 мс

Время расчета для английского: 11 мс

Ответы

Ответ 1

Я немного вырыл и нашел следующее:

Из источника FontDesignMetrics мы можем видеть последовательность основных действий

public int stringWidth(String str) {
float width = 0;
if (font.hasLayoutAttributes()) {
    /* TextLayout throws IAE for null, so throw NPE explicitly */
    if (str == null) {
        throw new NullPointerException("str is null");
    }
    if (str.length() == 0) {
        return 0;
    }
    width = new TextLayout(str, font, frc).getAdvance();
} else {
    int length = str.length();
    for (int i = 0; i < length; i++) {
        char ch = str.charAt(i);
        if (ch < 0x100) {
            width += getLatinCharWidth(ch);
        } else if (FontManager.isNonSimpleChar(ch)) {
            width = new TextLayout(str, font, frc).getAdvance();
            break;
        } else {
            width += handleCharWidth(ch);
        }
    }
}
return (int) (0.5 + width);

}

Для латинских символов используется метод getLatinCharWidth (ch). Он кэширует все ширины символов. Но для персидских и арабских символов TextLayout используется вместо. Основная цель состоит в том, что восточные символы могут иметь форму и ширину varios, зависящие от контекста. Можно добавить метод, который будет кэшировать ширину символов, но он не даст точных значений, например, он будет игнорировать нюансы различной ширины символов. Также он будет игнорировать различные лигатуры.

Я тестировал TextLayout отдельно, и он медленный для обоих языков английский и персидский. Поэтому реальной причиной медленной работы является медленная работа класса sun.font.TextLayout. Он используется для определения ширины строки в случае, если символы в строке не просты. К сожалению, я не знаю, как повысить производительность TextLayout на данный момент.

Если кому-то интересно, статья о различных тонкостях шрифта и текста http://download.oracle.com/javase/1.4.2/docs/guide/2d/spec/j2d-fonts.html

Ответ 2

Я провел несколько тестов с другими языками, используя ваш код. Сначала вы правы: расчеты персидской струны занимали много времени.

Я играл с типом и размером шрифта и не видел существенных различий. Но результат определенно зависит от используемого script. Вот результаты, которые я получил на своей машине.

Calculation time for Persian: 2877 ms
Calculation time for English: 8 ms
Calculation time for Russian: 47 ms
Calculation time for Hebrew:  16815 ms

Как вы видите, русский язык в 6 раз медленнее английского. Я считаю, что это потому, что внутреннее представление строк является unicode. В UTF-8 английские символы занимают один байт, остальные 2 байта.

Я не уверен, что он может вас удовлетворить:), но ивритский тест в 4 раза медленнее, чем персидский. Оба медленные, поэтому я думаю, что вычисления справа налево убивают его.

Кажется, что мы не имеем ничего общего с этим.

Ответ 3

Не могли бы вы использовать метод класса Font. public GlyphVector layoutGlyphVector (FontRenderContext frc,                                         char [] текст,                                        int start,                                        int limit,                                        int flags)

ANd использовать GlyphVector для измерения вашей строки?

Или TextLayout  public TextLayout (строка String, шрифт шрифта, FontRenderContext frc)

Ответ 4

Я использую кеш при расчете ширины строк. Он не разрешает внутренние вызовы, которые выполняют классы javas, но он решил мои проблемы с персидскими буквами (я использую много собственных рендерингов и т.д.). Класс Pair - это всего лишь типизированный bean из двух объектов...

public class GuiUtils {
private static final Map<Pair<Boolean, Pair<FontMetrics, String>>, Integer> stringWidthCache = new HashMap<Pair<Boolean, Pair<FontMetrics, String>>, Integer>();

public static int getStringWidth(FontMetrics fm, String text){
    return getStringWidth(null, fm, text);
}

public static int getStringWidth(Graphics g, FontMetrics fm, String text){
    if(text == null || text.equals("")) {
        return 0;
    }
    Pair<Boolean, Pair<FontMetrics, String>> cacheKey = 
            new Pair<Boolean, Pair<FontMetrics, String>>(g != null, new Pair<FontMetrics, String>(fm, text));
    if (!stringWidthCache.containsKey(cacheKey)) {
        stringWidthCache.put(
                cacheKey, 
                g != null ? 
                        (int)Math.ceil(fm.getStringBounds(text, g).getWidth()) :
                            fm.stringWidth(text));
    }
    return stringWidthCache.get(cacheKey);
}

}