Core Java Tech Tips
   
Tech Tips archive

Tech Tips
2007 年 2月 7日

2007 年 2 月号の Core Java Technologies Tech Tips をご購読いただき誠にありがとうございます。Core Java Technologies Tech Tips は、Java Platform、Standard Edition (Java SE) で提供されている Java のコアテクノロジと API の使用上のヒントを紹介します。

この号には次の内容に関するヒントが掲載されています。

» テキストの正規化
» Image I/O イベントの監視

注: このドキュメントのテキストは UTF-8 でエンコードされています。適切に表示されない文字がある場合は、ブラウザのエンコーディング設定の変更が必要な場合があります。

テキストの正規化

執筆者: Sergey Groznyh

テキストの正規化とは、事前定義された規則と一貫性を持たせるようにテキストを変換することです。たとえば、空白の削除、句読文字の削除、大文字/小文字の変換などがあります。この Tech Tip では、テキスト正規化の重要な形式の 1 つ、「Unicode テキストの正規化」について説明します。この記事では特に指定のない限り、Unicode という語は Unicode 4.0 を指します。Java SE 6 プラットフォームでは Unicode のこのバージョンがサポートされているためです。

Unicode 標準は、文字および文字連続体の間の 2 種類の等価を定義します。正規等価 (canonical equivalence) および互換等価 (compatibility equivalence) です。正規等価の例の 1 つに、合成済み文字およびそれと等価な結合シーケンスがあります。たとえば、Unicode 文字 'Ç' (LATIN CAPITAL LETTER C WITH CEDILLA) は、Unicode キャラクタ値 U+00C7 を持っています。また、Unicode の文字連続体 U+0043 U+0327 でも 'Ç' の文字が生成されます。この文字連続体には、LATIN CAPITAL LETTER C のキャラクタ値が含まれ、そのあとに COMBINING CEDILLA のキャラクタ値が含まれています。この単一の文字と文字連続体は正規等価です。視覚上では区別できず、テキストの比較および表示の用途では完全に同じものを意味するためです。

一方、互換等価では、同じ文字または文字連続体の代替視覚表現を定義する従来の文字セットを、主に取り扱っています。互換等価の例は、DIGIT TWO 文字 '2' (U+0032) および SUPERSCRIPT TWO 文字 '²' (U+00B2) です。どちらの文字も ISO/IEC 8859-1 (Latin1) の文字セットに含まれています。DIGIT TWO 文字 '2' と SUPERSCRIPT TWO 文字 '²' は、同じ基本文字のバリエーションなので互換等価です。これらの文字は視覚上区別でき、追加の意味情報を持っているので、正規等価ではありません。

Unicode テキストの正規化は、文字および文字連続体を、ある形式から別の等価な形式に翻訳するプロセスです。Unicode には 4 つの正規化標準が定義されています。

NFC
正規合成と呼ばれる正規化形式です。(Normalization Form Canonical Composition) 文字は正規等価によって分解されたあとで再合成されます。たとえば、「文字 + 結合マーク」のような文字列から、可能な場合は、1 つの文字を合成します。
NFD
正規合成と呼ばれる正規化形式です。(Normalization Form Canonical Decomposition) 文字は正規等価によって分解されます。たとえば、合成済みの文字 'Ç' (U+00C7) は、基本文字と結合アクセント記号からなる結合文字列に変換されます。
NFKC
互換合成と呼ばれる正規化形式です。(Normalization Form Compatibility Composition) 文字は互換等価によって分解されたあとで、正規等価によって再合成されます。
NFKD
互換分解と呼ばれる正規化形式です。(Normalization Form Compatibility Decomposition) 文字は互換等価によって分解されます。たとえば、分数 '½' (U+00BD) は 1/2 という 3 つの文字からなる文字列に変換されます。

Normalizer クラス

Java SE 6 では、公開クラスになった java.text.Normalizer を提供し、Unicode テキストの正規化をサポートします。このクラスには、テキストを変換する normalize メソッドと、Unicode 正規化形式である NFC、NFD、NFKC、NFKD を表す Form 列挙が定義されています。

さまざまな Unicode 正規形式のアプリケーションの例を次に示します。

例: NFC

Web にドキュメントを発行するとします。Character Model for the World Wide Web 仕様では、Web のインデックス作成、検索、およびその他のテキスト関連機能を改善するために、データを発行する前に正規化することを推奨しています (早期正規化 (early normalization))。この仕様では NFC を推奨しています。その理由は、最近のソフトウェアによって作成されたデータだけでなくほとんどすべての従来のデータが、すでに NFC で正規化されているからです。次のコードは標準入力からデータを読み取り、NFC で正規化したデータを標準出力に書き込みます。UTF-8 エンコーディングが入力と出力の両方に使用されます。

import java.io.*;
import java.text.Normalizer;
import java.text.Normalizer.Form;

public class NFC {
  public static void main(String[] args) {
    final String INPUT_ENC = "UTF-8";
    final String OUTPUT_ENC = "UTF-8";
    try {
      BufferedReader r = new BufferedReader(
          new InputStreamReader(System.in, INPUT_ENC));
      PrintWriter w = new PrintWriter(
          new OutputStreamWriter(System.out, OUTPUT_ENC), true);
      String s;
      while ((s = r.readLine()) != null) {
        w.println(Normalizer.normalize(s, Form.NFC));
      }
    } 
    catch (Exception ex) {
      ex.printStackTrace();
    }
  }
}
 

NFC 形式の正規化は、文字列等価テストにも適しています。文字列比較には、java.text.Collator クラスを適切なロケールで初期化して使用してください。Collator クラスを使用する理由は、アクセント記号付き文字のソート順が言語によって異なるためです。ソートする際に、アクセント記号付き文字をその基本文字の次に置く言語と、すべての基本文字の後にアクセント記号付き文字を置く言語があります。

例: NFD

電話帳アプリケーションを開発するとしましょう。電話帳データをあるデータベースに格納し、データを検索する検索フォームを設定します。世界中の人名にはアクセント記号付き文字が含まれているため、次の 2 つの問題が発生します。多くのデータベースがアクセント記号付き文字に対応していないということと、アプリケーションのユーザーの多くが正しい (アクセント記号付きの) 名前をアプリケーションの検索フォームにわざわざ入力しないか、あるいはそのような入力操作を行えないということです。このため、データベースに格納されたデータと検索フォームから読み取られたデータの両方から、すべてのアクセント記号を削除する必要があります。

次のコードは標準入力から 1 行ずつ読み取り、各行からアクセント記号付き文字を分離して、結果を標準出力に書き込みます。UTF-8 エンコーディングが入力と出力の両方に使用されます。

import java.io.*;
import java.text.Normalizer;
import java.text.Normalizer.Form;

public class NFD {
  public static void main(String[] args) {
    final String INPUT_ENC = "UTF-8";
    final String OUTPUT_ENC = "UTF-8";
    try {                
      BufferedReader r = new BufferedReader(
        new InputStreamReader(System.in, INPUT_ENC));
      PrintWriter w = new PrintWriter(
        new OutputStreamWriter(System.out, OUTPUT_ENC), true);
      String s;
      while ((s = r.readLine()) != null) {
        // decompose and remove accents
        String decomposed = Normalizer.normalize(s, Form.NFD);
        String accentsGone = 
            decomposed.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
        w.println(accentsGone);
      }
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }
}
 

例: NFKC

NFKC 形式の正規化は、互換分解の正規化形式を持つ結合マーク付きの文字に影響します。したがって、文字列 U+1E9B U+0323 (LATIN SMALL LETTER LONG S WITH DOT ABOVE とそれに続く COMBINING DOT BELOW) は、1 つのキャラクタ値 U+1E69 に変換されます。正規化された文字は '' (LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE) です。

この正規化形式は、国際ドメイン名の文字列プロファイル仕様 (RFC 3491) に準拠するために必要となります。ドメイン名に非 ASCII 文字が含まれる場合は、NFKC 形式に正規化する必要があります。国際ドメイン名を登録するアプリケーションを構築する場合は、ドメイン名をこの正規化形式にエンコードしてください。

国際ドメイン名のエンコーディングには Unicode バージョン 3.2 が指定されていますが、Unicode バージョン 3.2 と 4.0 の CJK 表意文字の一部では、正規化形式に違いがあります。RFC 3491 を実装せず、正規化されたドメイン名を取得するだけの場合は、java.net.IDN クラスで提供されている機能を使用することができます。

エンコーディングプロセスは NFC の例で示したプロセスと似ていますが、唯一の相違点は、Form.NFC の代わりに Form.NFKC エンコーディングフォームを使用する必要があるということです。

例: NFKD

この正規化形式は、従来のテキストデータを XML フォーマットに変換する際に役に立ちます。Unicode in XML and other Markup Languages 仕様には、互換性のある文字を処理するための規則がいくつか定義されています。たとえば、指数と添字での <sup> および <sub> マークアップの使用、分数を表すための MathML マークアップの使用、丸数字の代替としての並び項目マーカーの使用などが推奨されています。従来のデータを XML に変換するアプリケーションを構築する場合は、NFKD に正規化したテキストデータに対して、適切なマークアップ、スタイル、またはその両方を適用することを検討してください。

データを NFKD 正規化形式に変換するには、次のように Form.NFKDNormalizer.normalize メソッドに、2 番目のパラメータとして渡す必要があります。

Normalizer.normalize(s, Form.NFKD); 

正規化テスト

java.text.Normalizer クラスには、指定された文字列が 4 つの正規化形式のいずれかに従って正規化されているかどうかをチェックする isNormalized メソッドが定義されています。次のコードは標準入力から行を読み取り、その行が 4 つの正規化形式のいずれかに正規化されているかどうかを報告します。入力は UTF-8 エンコードです。

import java.io.*;
import java.text.Normalizer;
import java.text.Normalizer.Form;

public class IsNormalized {
  public static void main(String[] args) {
    final String INPUT_ENC = "UTF-8";
    final Form[] forms = { Form.NFC, Form.NFD, Form.NFKC, Form.NFKD };
    try {                
      BufferedReader r = new BufferedReader(
          new InputStreamReader(System.in, INPUT_ENC));
      String s;
      int line = 1;
      while ((s = r.readLine()) != null) {
        System.out.printf("%5d:", line++);
        for (Form f : forms) {
          if (Normalizer.isNormalized(s, f)) {
            System.out.print(" " + f.toString());
          }
        }
        System.out.println();
      }
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }
}
 

関連情報

正規化については、次のドキュメントを参照してください。

Image I/O イベントの監視

執筆者: Brian Burkhalter

JavaTM Image I/O API は Java プラットフォームでイメージを処理するフレームワークを提供します。このフレームワークに含まれる javax.imageio.event パッケージ には、イメージを読み取ったり書き込んだりする際に発行される同期イベントを監視するためのインタフェースがいくつか定義されています。

これらのインタフェースは次の目的に使用できます。

  1. イメージの読み取りの進行状況を監視する
  2. イメージの各領域を読み取る際に通知を受け取る
  3. イメージの読み取りに関する警告メッセージを捕捉する

このようなさまざまなイベント監視は、使用している ImageReader にリスナーを登録すると、それぞれ使用可能になります。ImageReader インスタンスは ImageIO クラスから取得してください。次のコードは、TIFF イメージフォーマットのリーダーを取得する方法を示しています。

ImageReader reader;
Iterator<ImageReader> readers = 
  ImageIO.getImageReadersByMIMEType("image/tiff");
if (readers.hasNext()) {
  reader = readers.next();
}
 

読み取りの進行状況の監視

IIOReadProgressListener インタフェースは、イメージの読み取りの進行状況を監視するために使用されます。IIOReadProgressListener の実装を ImageReader に登録するには、addIIOReadProgressListener メソッドを使用します。たとえば次のコードは、イメージの読み取りの進行状況を表示するために、ProgressMonitor を使用しています。

Component parentComponent;
String imageName;
ImageReader reader;

// Create a ProgressMonitor which displays percentage completed.
final ProgressMonitor pm =
    new ProgressMonitor(parentComponent, imageName, "0 %", 0, 100);

// Register an anonymous inner class implementing IIOReadProgressListener
reader.addIIOReadProgressListener(new IIOReadProgressListener() {
  // Close the ProgressMonitor if the read is aborted.
  public void readAborted(ImageReader source) {
    pm.close();
  }

  public void imageStarted(ImageReader source,
      int imageIndex) {
    // Abort the read if "cancel" pressed.
    if(pm.isCanceled()) {
      source.abort();
    }
  }

  // Set image progress to 100% upon completion.
  public void imageComplete(ImageReader source) {
    imageProgress(source, 100.0F);
  }
  
  // Update the progress bar and its label each time the reader
  // notifies the IIOReadProgressListener of a new percentage.
  public void imageProgress(ImageReader source, float percentageDone) {
    // Abort the read if "cancel" pressed.
    if(pm.isCanceled()) {
      source.abort();
      return;
    }
  
    // Update the progress and label.
    final int nv = (int)percentageDone;
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        pm.setProgress(nv);
        pm.setNote(nv+" %");
      }
    });
  }
  
  public void thumbnailStarted(ImageReader source, int imageIndex, 
      int thumbnailIndex) { 
  }
  
  public void thumbnailProgress(ImageReader source, float percentageDone) {}
  
  public void thumbnailComplete(ImageReader source) {}
  
  public void sequenceStarted(ImageReader source, int minIndex) {}
  
  public void sequenceComplete(ImageReader source) {}
});
 

ProgressMonitor は、イメージのロードが進むにつれて、左から右に充てんされる進捗バーを表示します。イメージの読み取りを開始する際に、リーダーが imageStarted メソッドを呼び出します。リスナーは ProgressMonitor の 「取り消し (Cancel)」ボタンが押されたかどうかをチェックします。キャンセルすると、読み取りが停止します。キャンセルした場合、リーダーが readAborted メソッドを呼び出し、さらにこのメソッドが ProgressMonitor を閉じます。

読み取りの間、リーダーは更新される進行値を指定して imageProgress メソッドを定期的に呼び出します。リスナーは、読み取りが取り消されたかどうかをチェックしたあとで、ProgressMonitor を新しい進行値とパーセンテージ値文字列で更新します。リスナーが ProgressMonitor を更新するのは imageProgress メソッドを呼び出したスレッド上ではなく、Swing イベントスレッド上です。イメージの読み取りが完了すると、リーダーは imageComplete メソッドを呼び出して完了パーセンテージ値を 100 に設定します。

次のイメージは、進捗バーを更新している ProgressMonitor を示します。

ProgressMonitor

サムネールのロードの進行状況を監視する場合は、IIOReadProgressListener クラスのサムネール関連メソッドを使用できます。

領域更新通知の受け取り

イメージの各領域が読み取られる際の通知を受け取るために、IIOReadUpdateListener インタフェースが使用されます。1 つのイメージ領域は、走査線を使用するイメージではピクセルからなる 1 行、タイルイメージでは 1 つのタイルです。IIOReadUpdateListener 実装を ImageReader インスタンスに登録するには、addIIOReadUpdateListener メソッドを使用します。

IIOReadProgressListener はイメージ表示コンポーネントで、たとえば次のように実装できます。

public void imageUpdate(ImageReader reader, BufferedImage image,
    int minX, int minY, int width, int height,
    int periodX, int periodY, int[] bands) {
  // Set the displayed image to the parameter image.
  setImage(image);
  
  // Repaint the sections of the image just updated.
  repaint(0L, minX, minY, width, height);
}
  
public void passStarted(ImageReader reader, BufferedImage image,
    int pass, int minPass, int maxPass,
    int minX, int minY,
    int periodX, int periodY, int[] bands) {}
      
public void passComplete(ImageReader reader, BufferedImage image) {}
      
public void thumbnailPassStarted(ImageReader reader, BufferedImage image,
    int pass, int minPass, int maxPass,
    int minX, int minY,
    int periodX, int periodY, int[] bands) {}
          
public void thumbnailPassComplete(ImageReader reader, 
    BufferedImage image) {}
 

ImageReaderimageUpate メソッドを呼び出すと、このメソッドによってまず setImage メソッドが呼び出されて、表示コンポーネントに必要な任意の初期化が行われます。

/** Instance variable for the image being displayed. */
BufferedImage theImage = null;

/** Initialize the display component using the provided image. */
protected void setImage(BufferedImage image) {
  if(image != theImage) {
    // Save the reference.
    theImage = image;
    // Initialize the display component as needed.
    // --- CODE OMITTED ---
  }
}        
 

次に、表示コンポーネントは repaint メソッドを呼び出し、ロードされたばかりのイメージ領域を描画します。

イメージ領域が更新されるときに通知を受けることは、大きなイメージを読み取る際に特に重要です。更新通知では、イメージの各領域が読み取られると同時に、表示コンポーネントは領域を描画でき、イメージ全体がロードされるまで待つ必要はありません。

警告メッセージの捕捉

イメージを読み取る際に ImageReader によって発行される警告メッセージを捕捉するには、IIOReadWarningListener インタフェースが使用されます。IIOReadWarningListener の実装を ImageReader に登録するには、addIIOReadWarningListener() メソッドを使用します。たとえば次のコードでは、警告メッセージのテキストを表示するために JDialog を使用しています。

Component parentComponent;
String imageName;
ImageReader reader;

// Create a text area to contain the warning message(s).
final JTextArea text = new JTextArea();
text.setColumns(60);
text.setLineWrap(true);
text.setWrapStyleWord(true);

// Create a warning option pane to contain the text area.
JOptionPane opt = new JOptionPane(new JScrollPane(text),
    JOptionPane.WARNING_MESSAGE, JOptionPane.DEFAULT_OPTION);

// Create a modal dialog for the option pane.
final JDialog dialog =
    opt.createDialog(JOptionPane.getFrameForComponent(parentComponent),
    "Warnings: "+imageName);
dialog.setModal(false);

// Register an anonymous inner class implementing IIOReadWarningListener
reader.addIIOReadWarningListener(new IIOReadWarningListener() {
  public void warningOccurred(ImageReader source,
      final String warning) {
    // Append the current warning to the text area.
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        text.append("[WARNING]: "+ warning+"\n");
        dialog.pack();
        dialog.setVisible(true);
      }
    });
  }
});
 

ImageReader は警告メッセージを発行する場合、warningOccured メソッドを呼び出してメッセージをテキスト領域に追加し、警告ダイアログを表示します。警告ダイアログが更新されるのは Swing イベントスレッド上です。warningOccurred メソッドを呼び出したスレッド上ではありません。

次のイメージは警告ダイアログを示しています。

警告ダイアログ

イメージ書き込みイベントの監視

イメージを書き込む際の Image I/O イベントの監視は、イメージを読み取る際のイベントの監視と酷似しています。IIOWriteProgressListener および IIOWriteWarningListener インタフェースは、それぞれ IIOReadProgressListener および IIOReadWarningListener と相似です。IIOWriteProgressListener および IIOWriteWarningListener インタフェースを ImageWriter で使用する方法については、IIOReadProgressListener および IIOReadWarningListener インタフェースを ImageReader で使用する方法と似ていますので、上記の説明を参照してください。

関連情報

Image I/O API については、次のドキュメントを参照してください。