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

У меня проблема. Мой интерфейс приложения работает намного медленнее, если я использую там восточные языки. Особенно я чувствовал это в таких компонентах, как 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 мс

4 ответа

Я немного покопался и нашел следующее:

Из источника 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 используется. Основная цель заключается в том, что восточные символы могут иметь различную форму и ширину в зависимости от контекста. Можно добавить метод, который будет кэшировать ширину символов, но он не будет давать точных значений, так как он будет игнорировать нюансы различной ширины символов. Также он будет игнорировать различные лигатуры.

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

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

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

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

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 раза медленнее, чем на персидском. Оба медленные, поэтому я предполагаю, что вычисления справа налево убивают это.

Кажется, мы не имеем к этому никакого отношения.

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

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

Или TextLayout общедоступный TextLayout(строка String, шрифт Font, FontRenderContext frc)

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

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);
}

}

Другие вопросы по тегам