7

JFreeChart でグラフをレンダリングするときに、グラフのカテゴリ ラベルに日本語の文字が含まれていると、レイアウトの問題が発生することに気付きました。テキストは正しいグリフでレンダリングされますが、おそらくフォント メトリックが間違っていたため、テキストが間違った場所に配置されていました。

グラフは、もともとそのテキストにSource Sans Pro Regularフォントを使用するように構成されており、ラテン文字セットのみをサポートしています。明らかな解決策は、実際の日本語の .TTF フォントをバンドルして、JFreeChart に使用を依頼することです。出力テキストが正しいグリフを使用し、正しくレイアウトされているという点で、これはうまく機能します。

私の質問

  • 最初のシナリオで、ラテン文字以外を実際にサポートしていないソース フォントを使用している場合、java.awt はどのようにして日本語の文字を正しくレンダリングしたのでしょうか? 問題があれば、JDK 1.7u45 を使用して OS X 10.9 でテストしています。

  • 別の日本語フォントをバンドルせずに日本語の文字をレンダリングする方法はありますか? (これが私の最終目標です!) バンドル ソリューションは機能しますが、回避できるのであれば、アプリケーションに 6 MB の肥大化を追加したくありません。Java は、フォントがなくても (少なくとも私のローカル環境では) どうにかして日本語のグリフをレンダリングする方法を明確に知っています。これが以下の「frankenfont」の問題に関連しているかどうか疑問に思っています。

  • JRE が内部変換を実行した後、なぜ Source Sans Pro フォントは ( canDisplayUpTo()を介して) 呼び出し元に、日本語の文字を表示できないのに表示できると通知するのでしょうか? (下記参照。)

明確にするために編集:

  • これはサーバー アプリであり、レンダリングしているテキストはクライアントのブラウザーや PDF エクスポートに表示されます。グラフは常にサーバー上で PNG にラスタライズされます。

  • サーバーの OS や環境を制御することはできません。Java 標準のプラットフォーム フォントを使用するのは良いことですが、多くのプラットフォームではフォントの選択が貧弱であり、私のユース ケースでは受け入れられないため、独自のフォントをバンドルする必要があります (少なくともラテン フォントの場合)。日本語テキストにプラットフォーム フォントを使用してもかまいません。

  • アプリは、テキスト タイプの先験的な知識がなくても、日本語とラテン語のテキストを組み合わせて表示するように求められる可能性があります。グリフが正しくレンダリングされる限り、文字列に言語が混在している場合にどのフォントが使用されるかについて、私は曖昧です。

詳細

java.awt.Font#TextLayout はスマートであり、テキストをレイアウトしようとすると、最初に下層のフォントに提供された文字を実際にレンダリングできるかどうかを問い合わせることを理解しています。そうでない場合は、おそらくそれらの文字をレンダリングする方法を知っている別のフォントにスワップしますが、JRE クラスにかなり深くまでデバッグしたことに基づいて、ここでは発生していません。TextLayout#singleFont常にフォントの null 以外の値を返し、fastInit()コンストラクターの一部を処理します。

非常に興味深い点の 1 つは、Source 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

上記の「魔法の操作」の 3 行は、私自身が行ったものではないことに注意してください。真のソース フォント オブジェクトを JFreeChart に渡しますが、グリフを描画するときに JRE によって変更されます。これは、上記の「魔法の操作」コードの 3 行が複製するものです。上記の操作は、次の一連の呼び出しで発生するものと機能的に同等です。

  1. org.jfree.text.TextUtilities#drawRotatedString
  2. sun.java2d.SunGraphics2D#drawString
  3. java.awt.font.TextLayout#(コンストラクタ)
  4. java.awt.font.TextLayout#singleFont

「魔法の」操作の最後の行で Font.getFont() を呼び出すと、Source Sans Pro フォントが返されますが、基になるフォントのfont2Dフィールドは元のフォントとは異なり、この単一のフォントは認識していると主張します。文字列全体をレンダリングする方法。なんで?Java は、基礎となるソース フォントで提供されるグリフのメトリックしか理解できないにもかかわらず、あらゆる種類のグリフをレンダリングする方法を知っているある種の「frankenfont」を私たちに返しているようです。

JFreeChart のレンダリング例を示すより完全な例は、JFreeChart の例の 1 つに基づいています: https://gist.github.com/sdudley/b710fd384e495e7f1439この例の出力を以下に示します。

Source Sans Pro フォントの例 (レイアウトが正しくありません):

ここに画像の説明を入力

IPA 日本語フォントの例 (正しくレイアウトされています):

ここに画像の説明を入力

4

2 に答える 2

5

私はついにそれを理解しました。いくつかの根本的な原因がありましたが、クロスプラットフォームの変動性が追加されたことでさらに妨げられました.

JFreeChart は異なるフォント オブジェクトを使用するため、テキストを間違った場所にレンダリングする

JFreeChart が、AWT が実際にフォントのレンダリングに使用するものとは異なる Font オブジェクトを使用してレイアウトのメトリックを誤って計算していたため、レイアウトの問題が発生しました。(参考までに、JFreeChart の計算は で行われorg.jfree.text#getTextBoundsます。)

異なる Font オブジェクトの理由は、質問で言及されている暗黙の「魔法の操作」の結果であり、これは の内部で実行されjava.awt.font.TextLayout#singleFontます。

これらの 3 行の魔法操作は、次のように要約できます。

font = Font.getFont(font.getAttributes())

英語では、提供されたフォントの「属性」(名前、ファミリ、ポイント サイズなど) に基づいて新しい Font オブジェクトを提供するようにフォント マネージャーに要求します。特定の状況下では、それがあなたに返すものは、あなたが最初に始めたものとFontは異なります.Font

メトリックを修正する (したがってレイアウトを修正する) には、JFreeChart オブジェクトでフォントを設定する前に、独自のオブジェクトで上記のワンライナーを実行しFontます。

これを行った後、日本語の文字と同様に、レイアウトはうまくいきました。日本語の文字が正しく表示されない場合がありますが、レイアウトも修正されるはずです。その理由を理解するには、ネイティブ フォントについて以下をお読みください。

Mac OS X フォント マネージャーは、物理 TTF ファイルをフィードしてもネイティブ フォントを返すことを好む

上記の変更により、テキストのレイアウトは修正されましたが、これはなぜでしょうか? どのような状況で、FontManager は、Font提供したものとは異なるタイプのオブジェクトを実際に返すのでしょうか?

多くの理由がありますが、少なくとも Mac OS X では、問題に関連する理由は、フォント マネージャーが可能な限りネイティブ フォントを返すことを好むように見えることです。

つまり、 を使用して「Foobar」という名前の物理 TTF フォントから新しいフォントを作成し、「Font.createFontFoobar」物理フォントから派生した属性で Font.getFont() を呼び出すと、OS X に既にFoobar フォントがインストールされている場合、フォント マネージャーは、期待していたCFontオブジェクトではなく、オブジェクトを返します。を通じてフォントを登録しても、TrueTypeFontこれは当てはまるようです。GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont

私の場合、これは調査に厄介な問題を投げかけました: 私の Mac には既に "Source Sans" フォントがインストールされていました。

Mac OS X ネイティブ フォントは常にアジア文字をサポート

問題の核心は、Mac OS XCFontオブジェクトは常にアジアの文字セットをサポートするということです。これを可能にする正確なメカニズムは不明ですが、Java ではなく、OS X 自体の何らかのフォールバック フォント機能であると思われます。どちらの場合でも、 a はCFont常にアジア文字を正しいグリフでレンダリングすると主張しています (実際にレンダリングすることができます)。

これにより、元の問題が発生したメカニズムが明らかになります。

  • Font日本語をサポートしていない物理 TTF ファイルから物理ファイルを作成しました。
  • 上記と同じ物理フォントが私の Mac OS X Font Book にもインストールされました
  • チャートのレイアウトを計算するとき、JFreeChart は物理Fontオブジェクトに日本語テキストのメトリックを尋ねました。アジアの文字セットをサポートしていないため、この物理Fontはこれを正しく行うことができませんでした。
  • 実際にチャートを描画するとき、魔法の操作TextLayout#singleFontによりオブジェクトが取得さCFontれ、物理的なTrueTypeFont. したがって、グリフは正しいのですが、適切に配置されていませんでした。

フォントを登録したかどうか、およびフォントが OS にインストールされているかどうかによって、異なる結果が得られます。

作成された TTF フォントの属性を使用して呼び出すFont.getFont()と、フォントが登録されているかどうか、および同じフォントがネイティブにインストールされているかどうかに応じて、次の 3 つの異なる結果のいずれかが得られます。

  • TTF フォントと同じ名前のネイティブ プラットフォーム フォントがインストールされている場合(フォントを登録したかどうかに関係なく)、必要なCFontフォントのアジア サポートが取得されます。
  • GraphicsEnvironmentに TTF を登録したFontが、同じ名前のネイティブ フォントがない場合、Font.getFont() を呼び出すと、物理TrueTypeFontオブジェクトが返されます。これにより、必要なフォントが得られますが、アジアの文字は得られません。
  • TTFFontを登録しておらず、同じ名前のネイティブ フォントも持っていない場合、Font.getFont() を呼び出すと、アジア言語をサポートする CFont が返されますが、それは要求したフォントではありません。

後から考えると、これはまったく驚くべきことではありません。につながる:

誤って間違ったフォントを使用していた

本番アプリでフォントを作成していたのですが、最初に GraphicsEnvironment に登録するのを忘れていました。上記の魔法の操作を実行するときにフォントを登録していない場合、Font.getFont()はそれを取得する方法を知らず、代わりにバックアップ フォントを取得します。おっとっと。

Windows、Mac、および Linux では、このバックアップ フォントは通常、アジア文字をサポートする論理 (複合) フォントである Dialog のようです。少なくとも Java 7u72 では、Dialog フォントのデフォルトは以下の西洋アルファベットのフォントです。

  • マック:ルシダ・グランデ
  • Linux (CentOS): Lucida Sans
  • 窓: アリアル

この間違いは、実際にはアジアのユーザーにとっては良いことでした。なぜなら、彼らの文字セットが論理フォントで期待どおりにレンダリングされることを意味していたからです。

それは間違ったフォントでレンダリングされていたので、とにかく日本語のレイアウトを修正する必要があったため、将来のリリースでは 1 つの共通フォントに標準化することを試みたほうがよいと判断しました (したがって、trashgod の提案に近づきました)。

さらに、アプリには、特定のフォントの使用を常に許可するとは限らないフォント レンダリング品質要件があるため、オラクルがJava のすべてのコピー。しかし...

Lucida Sans は、すべてのプラットフォームでアジアの文字とうまく動作しません

Lucida Sans を使用するという決定は妥当に思えましたが、Lucida Sans の処理方法にはプラットフォームの違いがあることがすぐにわかりました。Linux と Windows では、"Lucida Sans" フォントのコピーを要求すると、物理的なTrueTypeFontオブジェクトが得られます。しかし、そのフォントはアジア文字をサポートしていません。

「Lucida Sans」を要求すると、Mac OS X でも同じ問題が発生します...しかし、わずかに異なる名前「LucidaSans」を要求すると (スペースの不足に注意してください)、CFontLucida Sans もサポートするオブジェクトが得られます。アジアのキャラクターなので、ケーキを持って食べることもできます。

他のプラットフォームでは、"LucidaSans" を要求すると、標準の Dialog フォントのコピーが生成されます。これは、そのようなフォントがなく、Java がデフォルトを返すためです。Linux では、Dialog は実際にはデフォルトで西洋のテキストに Lucida Sans を使用するため (また、アジアの文字にはまともなフォールバック フォントを使用するため)、ここでは多少幸運です。

これにより、次の名前のフォントを要求することで、すべてのプラットフォームで (ほぼ) 同じ物理フォントを取得し、アジア文字もサポートするパスが得られます。

  • Mac OS X: "LucidaSans" (Lucida Sans + アジアのバックアップ フォントを生成)
  • Linux: "Dialog" (Lucida Sans + アジアのバックアップ フォントを生成)
  • Windows: "Dialog" ( Arial + Asian バックアップ フォントを生成)

Windows の fonts.properties を詳しく調べたところ、Lucida Sans にデフォルト設定されたフォント シーケンスが見つかりませんでした。そのため、Windows ユーザーは Arial で行き詰まる必要があるようです...しかし、少なくとも視覚的にはそれほど違いはありません。 Lucida Sans から入手でき、Windows フォントのレンダリング品質は妥当です。

すべてはどこに行き着いたのか?

要するに、私たちは現在ほとんどプラットフォーム フォントを使用しているだけです。(@trashgod は今、良い笑いを浮かべているに違いありません!) Mac と Linux サーバーの両方に Lucida Sans があり、Windows には Arial があり、レンダリング品質は良好で、誰もが満足しています!

于 2014-11-21T22:27:19.440 に答える
3

あなたの質問に直接対処するものではありませんが、飾り気のないグラフでプラットフォームのデフォルト フォントを使用して結果を表示するための参考になると思います。sourceの簡略版をBarChartDemo1以下に示します。

サードパーティのフォント メトリクスは気まぐれなので、プラットフォームのサポートされているロケールに基づいて選択される、プラットフォームの標準論理フォントから逸脱しないようにしています。論理フォントは、プラットフォームの構成ファイルで物理フォントにマップされます。Mac OS では、関連するファイルは にあります。$JAVA_HOME/jre/lib/ここで、$JAVA_HOMEは評価の結果で /usr/libexec/java_home -v 1.nあり、nはバージョンです。バージョン 7 または 8 で同様の結果が得られます。特に、fontconfig.properties.src日本語フォント ファミリのバリエーションを提供するために使用されるフォントを定義します。すべてのマッピングはMS Minchoorを使用しているよう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);
        });
    }
}
于 2014-09-29T00:09:11.640 に答える