Почему шрифт Java, состоящий только из латинских символов, утверждает, что поддерживает азиатские символы, хотя это не так?
При рендеринге диаграммы с помощью JFreeChart я заметил проблему компоновки, когда метки категорий диаграммы включали японские символы. Хотя текст отображается с правильными символами, текст был расположен в неправильном месте, вероятно, потому, что метрики шрифта были неправильными.
Диаграмма изначально была настроена на использование шрифтаSource Sans Pro Regular для этого текста, который поддерживает только латинские наборы символов. Очевидное решение состоит в том, чтобы связать настоящий японский .Шрифт TTF и попросите JFreeChart использовать его. Это работает отлично, в том, что выходной текст использует правильные глифы, и он также выложен правильно.
Мои вопросы
-
Как же Ява.awt в конечном итоге правильно отображает японские символы в первом сценарии, когда используется исходный шрифт, который на самом деле не поддерживает ничего, кроме латинских символов? Если это имеет значение, я тестирую OS X 10.9 с JDK 1. 7u45.
-
Есть ли какой-нибудь способ передать японцев символы без связывания отдельным японским шрифтом? (Это моя конечная цель!) Хотя пакетное решение работает, я не хочу добавлять 6 Мб раздувания в мое приложение, если этого можно избежать. Java явно знает, как отрисовывать японские глифы каким-то образом даже без шрифта (по крайней мере, в моей локальной среде) - похоже, что это просто метрики, которые сломаны. Мне интересно, связано ли это с проблемой "франкенфонта" ниже.
-
После того, как JRE выполняет внутреннюю трансформацию, почему исходный шрифт Sans Pro сообщает вызывающему абоненту (через canDisplayUpTo () ), что он может отображать японские символы, хотя и не может? (Увидеть ниже.)
Отредактировано для уточнения:
-
Это серверное приложение, и текст, который мы отрисовываем, будет отображаться в браузере клиента и / или в экспорте PDF. Диаграммы всегда растеризуются в PNGs на сервере.
-
Я не имею никакого контроля над серверной ОС или окружающей средой, и как бы хорошо это ни было чтобы использовать стандартные шрифты платформы Java, многие платформы имеют плохой выбор шрифтов, который недопустим в моем случае использования, поэтому мне нужно собрать свой собственный (по крайней мере, для латинских шрифтов). Использование шрифта платформы для японского текста приемлемо.
-
Приложение потенциально может быть предложено для отображения смеси Японского и латинского текста, без каких-либо априорных знаний типа текста. Я неоднозначно отношусь к тому, какие шрифты используются, если строка содержит смешанные языки, пока глифы визуализируются правильно.
Подробности
Я понимаю, что java.ОУ.Font#TextLayout является умным, и что при попытке выложить текст, он сначала спрашивает базовые шрифты, могут ли они на самом деле отображать предоставленные символы. Если нет, он, вероятно, меняет местами другой шрифт, который знает, как отображать эти символы, но это не происходит здесь, основываясь на моей отладке довольно далеко в классах JRE. TextLayout#singleFont
всегда возвращает ненулевое значение для шрифта, и он проходит через fastInit()
часть конструктора.
Одно очень любопытное замечание заключается в том, что исходный шрифт Sans Pro каким-то образом принуждается к сообщению вызывающему объекту, что он действительно знает, как отображать японские символы после того, как JRE выполняет преобразование шрифта.
Например:
// We load our font here (download from the first link above in the question)
File fontFile = new File("/tmp/source-sans-pro.regular.ttf");
Font font = Font.createFont(Font.TRUETYPE_FONT, new FileInputStream(fontFile));
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(font);
// Here is some Japanese text that we want to display
String str = "クローズ";
// Should say that the font cannot display any of these characters (return code = 0)
System.out.println("Font " + font.getName() + " can display up to: " + font.canDisplayUpTo(str));
// But after doing this magic manipulation, the font claims that it can display the
// entire string (return code = -1)
AttributedString as = new AttributedString(str, font.getAttributes());
Map<AttributedCharacterIterator.Attribute,Object> attributes = as.getIterator().getAttributes();
Font newFont = Font.getFont(attributes);
// Eeek, -1!
System.out.println("Font " + newFont.getName() + " can display up to: " + newFont.canDisplayUpTo(str));
Выход из этого:
Font Source Sans Pro can display up to: 0
Font Source Sans Pro can display up to: -1
Обратите внимание, что три строки" магической манипуляции", упомянутые выше, не являются чем-то моим собственным; мы передаем объект true source font в JFreeChart, но при рисовании иероглифов он попадает под действие JRE, что и воспроизводится в трех строках кода "магических манипуляций", приведенных выше. Манипуляция, показанная выше, является функциональным эквивалентом того, что происходит в следующей последовательности вызовов:
- орг.jfree.текст.TextUtilities#drawRotatedString
- солнце.java2d.SunGraphics2D#кулиской
- java.ОУ.шрифт.TextLayout#(конструктор)
- java.ОУ.шрифт.TextLayout#singleFont
Когда мы зовем Шрифт.getFont () в последней строке" волшебной " манипуляции мы все еще получаем исходный шрифт Sans Pro, но поле базового шрифта font2D
отличается от исходного шрифта, и этот единственный шрифт теперь утверждает, что он знает, как отобразить всю строку. Почему? Похоже, что Java возвращает нам своего рода" франкенфонт", который знает, как отрисовывать все виды глифов, даже если он понимает только метрики для глифов, которые поставляются в базовом исходном шрифте.
A более полный пример, показывающий пример рендеринга JFreeChart, приведен здесь, основываясь на одном из примеров JFreeChart: https://gist.github.com/sdudley/b710fd384e495e7f1439 вывод из этого примера показан ниже.
Пример с исходным шрифтом Sans Pro (выложен неверно):
Пример с японским шрифтом IPA (выложен правильно):
2 ответа:
Я, наконец, понял это. Имелся ряд глубинных причин, которым еще больше мешала дополнительная доза кросс-платформенной вариабельности.
JFreeChart отображает текст в неправильном месте, потому что он использует другой объект шрифта
Проблема макета возникла из-за того, что JFreeChart непреднамеренно вычислял метрики для макета, используядругой объект шрифта , чем тот, который AWT фактически использует для отображения шрифта. (Для справки, Jfreechart's вычисление происходит в
org.jfree.text#getTextBounds
.)Причина для другого объекта шрифта является результатом неявной "магической манипуляции", упомянутой в вопросе, которая выполняется внутри
Эти три линии магических манипуляций можно свести к следующему:java.awt.font.TextLayout#singleFont
.font = Font.getFont(font.getAttributes())
На английском языке это требует от менеджера шрифтов предоставить нам новый объект шрифта, основанный на" атрибутах " (имя, семейство, размер точки и т. д.) поставляемого шрифта. При определенных обстоятельствах
Font
он дает возвращение к вам будет отличаться отFont
, с которого вы изначально начали.Чтобы исправить метрики (и, таким образом, исправить макет), исправление состоит в том, чтобы запустить однострочный выше на вашем собственном
После этого макет работал для меня отлично, как и японские иероглифы. Он также должен исправить макет для вас, хотя он может неправильно отображать японские символы для вас. Читайте ниже о родном языке шрифты, чтобы понять, почему.Font
объекте перед установкой шрифта в объектах JFreeChart.Диспетчер шрифтов Mac OS X предпочитает возвращать собственные шрифты, даже если вы передаете ему физический файл TTF
Расположение текста было исправлено вышеуказанным изменением...но почему это происходит? При каких обстоятельствах FontManager действительно вернет нам другой тип объекта
Font
, чем тот, который мы предоставили?Есть много причин, но, по крайней мере, на Mac OS X, причина, связанная с проблемой, заключается в том, что диспетчер шрифтов, похоже, предпочитают по возможности возвращать собственные шрифты .
Другими словами, если вы создаете новый шрифт из физического шрифта TTF с именем "Foobar", используяFont.createFont
, а затем вызываете Font.getFont () с атрибутами, производными от вашего физического" Foobar " font...so если в OS X уже установлен шрифт Foobar, диспетчер шрифтов вернет вам объектCFont
, а не объектTrueTypeFont
, который вы ожидали. Это кажется верным , даже если вы регистрируете шрифт черезGraphicsEnvironment.getLocalGraphicsEnvironment().registerFont
. В моем случае это бросило отвлекающий маневр в расследование: у меня уже был установлен шрифт "Source Sans" на моем Mac, что означало, что я получал разные результаты от людей, которые этого не делали.Собственные шрифты Mac OS X всегда поддерживают азиатские символы
Суть вопроса заключается в том, что Мак ОС Х
CFont
объекты всегда поддерживают азиатские наборы символов. Мне неясен точный механизм, который позволяет это, но я подозреваю, что это своего рода запасной вариант функция шрифта самой OS X, а не Java. В любом случае aCFont
всегда претендует на то, чтобы (и действительно может) отображать азиатские символы с правильными глиф-ми.Это ясно дает понять механизм, что позволило первоначально проблема возникает:
- мы создали физический
Font
из физического файла TTF, который сам по себе не поддерживает японский.- тот же физический шрифт, что и выше, также был установлен в моей книге шрифтов Mac OS X
- при расчете компоновки диаграммы, JFreeChart запросил у физического объекта
Font
метрики японского текста. ФизическийFont
не может сделать это правильно, потому что он не поддерживает азиатские наборы символов.- когда на самом деле рисовалась диаграмма, магические манипуляции в
TextLayout#singleFont
заставили ее получить объектCFont
и нарисовать глиф, используя тот же самый именованный родной шрифт, в отличие от физическогоTrueTypeFont
. Таким образом, глифы были правильными, но они не были расположены должным образом.Вы Получите Разные Результаты В зависимости от того, зарегистрирован ли шрифт и установлен ли он в вашей операционной системе
Если вы вызовете
Font.getFont()
с атрибутами из созданного шрифта TTF, вы получите один из трех различных результатов, в зависимости от того, зарегистрирован ли шрифт и установлен ли у вас тот же шрифт изначально:Оглядываясь назад, я не вижу в этом ничего удивительного. Ведущий к:
- Если у вас установлен шрифт native platform с тем же именем, что и у вашего шрифта TTF (независимо от того, зарегистрировали ли вы шрифт или нет), вы будете получите азиатскую поддержку
CFont
для шрифта, который вы хотели.- Если вы зарегистрировали TTF
Font
в GraphicsEnvironment, но у вас нет собственного шрифта с тем же именем, вызывающего шрифт.getFont() вернет физический объектTrueTypeFont
обратно. Это дает вам шрифт, который вы хотите, но вы не получаете азиатских символов.- Если вы не зарегистрировали TTF
Font
и у вас также нет собственного шрифта с тем же именем, вызывающего шрифт.getFont () вернет азиатскую поддержку CFont, но она не будет шрифт, который вы просили.Я случайно использовал не тот шрифт
В производственном приложении я создавал шрифт, но забыл изначально зарегистрировать его в GraphicsEnvironment. Если вы не зарегистрировали шрифт при выполнении описанных выше магических манипуляций,
Font.getFont()
не знает, как его восстановить, и вместо этого вы получаете резервный шрифт. Ой.В Windows, Mac и Linux эта резервная копия шрифт обычно выглядит как Dialog, который является логическим (составным) шрифтом, поддерживающим азиатские символы. По крайней мере, в Java 7u72 шрифт диалога по умолчанию имеет следующие шрифты для западных алфавитов:
- Mac: Lucida Grande
- Linux (CentOS): Lucida Sans
- Окна: Arial
Эта ошибка была на самом деле хорошей вещью для наших азиатских пользователей, потому что это означало, что их наборы символов отображаются, как и ожидалось, с помощью логического шрифта...хотя ... Западные пользователи не получали наборы символов, которые мы хотели.
Поскольку он был отрисован не теми шрифтами, и нам все равно нужно было исправить японскую компоновку, я решил, что мне лучше попытаться стандартизировать один общий шрифт для будущих выпусков (и таким образом приблизиться к предложениям trashgod).Кроме того, приложение имеет требования к качеству рендеринга шрифтов, которые не всегда позволяют использовать определенные шрифты, поэтому разумным решением, казалось бы, было чтобы попытаться настроить приложение для использования Lucida Sans, который является одним физическим шрифтом, включенным Oracle во все копии Java. Но...
Lucida Sans не очень хорошо играет с азиатскими персонажами на всех платформах
Решение попробовать использовать Lucida Sans казалось разумным...но я быстро обнаружил, что существуют различия в платформе в том, как Lucida Sans обрабатывается. В Linux и Windows, Если вы попросите копию шрифта Lucida Sans, вы получите физический объектTrueTypeFont
. Но этот шрифт не поддерживает азиатских персонажей.Та же проблема имеет место и в Mac OS X, если вы запросите "Lucida Sans"...но если вы попросите немного другое название "LucidaSans" (обратите внимание на отсутствие места), то вы получите объект
CFont
, который поддерживает Lucida Sans, а также Азиатские символы, так что вы можете получить свой торт и съесть его тоже.На других платформах запрос "LucidaSans" выдает копию стандартного диалогового шрифта, потому что такого шрифта нет, и Java возвращает его по умолчанию. На Linux, вам здесь немного повезло, потому что Dialog фактически по умолчанию использует Lucida Sans для западного текста (и он также использует приличный резервный шрифт для азиатских символов).
Это дает нам возможность получить (почти) один и тот же физический шрифт на всех платформах, который также поддерживает азиатские символы, запрашивая шрифты с такими именами:Я внимательно изучил шрифты.свойства В Windows, и я не смог найти последовательность шрифтов, которая по умолчанию используется Lucida Sans, поэтому, похоже, нашим пользователям Windows придется застрять с Arial...но, по крайней мере, это не так визуально отличается от Lucida Sans, и качество рендеринга шрифтов Windows является разумным.
- Mac OS X: "LucidaSans" (дает Lucida Sans + азиатские резервные шрифты)
- Linux: "Dialog" (вывод Lucida Sans + Asian backup шрифты)
- окна: "Диалог" (урожайность Ариал + азиатских шрифтов, резервное копирование)
Где Все Закончилось?
В общем, мы сейчас в значительной степени только с помощью шрифтов. (Я уверен, что @trashgod сейчас хорошо посмеивается!) Оба сервера Mac и Linux получают Lucida Sans, Windows получает Arial, качество рендеринга хорошее, и все довольны!
Хотя это не относится непосредственно к вашему вопросу, я подумал, что это может обеспечить полезную точку отсчета, чтобы показать результат, используя шрифт платформы по умолчанию в неприкрашенной диаграмме. Упрощенная версия
BarChartDemo1
, Источник , показан ниже.Из-за капризов сторонних метрик шрифтов я стараюсь не отклоняться от стандартных логических шрифтов платформы , которые выбираются на основе поддерживаемых языковых стандартов платформы. физический шрифт находится в конфигурационных файлах платформы . На Mac OS соответствующий файл находится в
$JAVA_HOME/jre/lib/
, где$JAVA_HOME
- результат вычисления/usr/libexec/java_home -v 1.n
и n - ваша версия. Я вижу аналогичные результаты с любой версией 7 или 8. В частности,fontconfig.properties.src
определяет шрифт, используемый для предоставления вариаций японского семейства шрифтов. Все отображения, по-видимому, используютMS Mincho
илиMS Gothic
.import java.awt.Dimension; import java.awt.EventQueue; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.PlotOrientation; import org.jfree.data.category.CategoryDataset; import org.jfree.data.category.DefaultCategoryDataset; import org.jfree.ui.ApplicationFrame; import org.jfree.ui.RefineryUtilities; /** * @see http://stackoverflow.com/a/26090878/230513 * @see http://www.jfree.org/jfreechart/api/javadoc/src-html/org/jfree/chart/demo/BarChartDemo1.html */ public class BarChartDemo1 extends ApplicationFrame { /** * Creates a new demo instance. * * @param title the frame title. */ public BarChartDemo1(String title) { super(title); CategoryDataset dataset = createDataset(); JFreeChart chart = createChart(dataset); ChartPanel chartPanel = new ChartPanel(chart){ @Override public Dimension getPreferredSize() { return new Dimension(600, 400); } }; chartPanel.setFillZoomRectangle(true); chartPanel.setMouseWheelEnabled(true); setContentPane(chartPanel); } /** * Returns a sample dataset. * * @return The dataset. */ private static CategoryDataset createDataset() { // row keys... String series1 = "First"; String series2 = "Second"; String series3 = "Third"; // column keys... String category1 = "クローズ"; String category2 = "クローズ"; String category3 = "クローズクローズクローズ"; String category4 = "Category 4 クローズ"; String category5 = "Category 5"; // create the dataset... DefaultCategoryDataset dataset = new DefaultCategoryDataset(); dataset.addValue(1.0, series1, category1); dataset.addValue(4.0, series1, category2); dataset.addValue(3.0, series1, category3); dataset.addValue(5.0, series1, category4); dataset.addValue(5.0, series1, category5); dataset.addValue(5.0, series2, category1); dataset.addValue(7.0, series2, category2); dataset.addValue(6.0, series2, category3); dataset.addValue(8.0, series2, category4); dataset.addValue(4.0, series2, category5); dataset.addValue(4.0, series3, category1); dataset.addValue(3.0, series3, category2); dataset.addValue(2.0, series3, category3); dataset.addValue(3.0, series3, category4); dataset.addValue(6.0, series3, category5); return dataset; } /** * Creates a sample chart. * * @param dataset the dataset. * * @return The chart. */ private static JFreeChart createChart(CategoryDataset dataset) { // create the chart... JFreeChart chart = ChartFactory.createBarChart( "Bar Chart Demo 1", // chart title "Category", // domain axis label "Value", // range axis label dataset, // data PlotOrientation.HORIZONTAL, // orientation true, // include legend true, // tooltips? false // URLs? ); return chart; } /** * Starting point for the demonstration application. * * @param args ignored. */ public static void main(String[] args) { EventQueue.invokeLater(() -> { BarChartDemo1 demo = new BarChartDemo1("Bar Chart Demo 1"); demo.pack(); RefineryUtilities.centerFrameOnScreen(demo); demo.setVisible(true); }); } }