Kapitel 11. Unicode

Unicode in PDF

Funktionieren die bisher beschriebenen Tests auch mit Inhalten, die nicht ISO-8859-1 sind, beispielsweise mit russischen, griechischen oder chinesischen Texten und Metadaten?

Eine schwierige Frage. Denn auch wenn bei der Entwicklung von PDFUnit viel Wert darauf gelegt wurde, generell mit Unicode zu funktionieren, kann eine pauschale Antwort nur gegeben werden, wenn die eigenen Tests für PDFUnit selber mit allen Möglichkeiten durchgetestet wurde. PDFUnit hat zwar etliche Tests für griechische, russische und chinesische Dokumente, aber für hebräische, arabische und japanische PDF-Dokumente nur wenig Tests. Insofern kann die eingangs gestellte Frage nicht abschließend beantwortet werden.

Die folgenden Tipps im Umgang mit UTF-8 Dateien lösen nicht nur Probleme im Zusammenhang mit PDFUnit. Sie sind sicher auch in anderen Situationen hilfreich.

Einzelne Unicode-Zeichen

Metadaten und Schlüsselwörter können Unicode-Zeichen enthalten. Wenn Ihre Entwicklungsumgebung die fremden Fonts nicht unterstützt, können Sie ein Unicode-Zeichen in Java mit \uXXXX schreiben, wie hier das Copyright-Zeichen © als \u00A9:

@Test 
public void hasProducer_CopyrightAsUnicode() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  AssertThat.document(filename)
            .hasProducer()
            .equalsTo("txt2pdf v7.3 \u00A9 SANFACE Software 2004") // 'copyright'
  ;
}

Längere Unicode-Texte

Es wäre nun zu mühsam, für längere Texte den Hex-Code aller Buchstaben herauszufinden. Deshalb stellt PDFUnit das kleine Programm ConvertUnicodeToHex zur Verfügung. Übergeben Sie den ausländischen Text als String an das Werkzeug, entnehmen Sie der daraus erzeugten Datei anschließend den Hex-Code und fügen ihn in Ihr Testprogramm ein. Eine genaue Beschreibung steht in Kapitel 9.12: „Unicode-Texte in Hex-Code umwandeln“. Das Test mit Unicode sieht dann so aus:

@Test 
public void hasSubject_Greek() throws Exception {
  String filename = "documentUnderTest.pdf";
  String expectedSubject = "Εργαστήριο Μηχανικής ΙΙ ΤΕΙ ΠΕΙΡΑΙΑ / Μηχανολόγοι";
//String expectedSubject =  "\u0395\u03C1\u03B3\u03B1\u03C3\u03C4\u03AE"
//                        + "\u03C1\u03B9\u03BF \u039C\u03B7\u03C7\u03B1"
//                        + "\u03BD\u03B9\u03BA\u03AE\u03C2 \u0399\u0399 "
//                        + "\u03A4\u0395\u0399 \u03A0\u0395\u0399\u03A1"
//                        + "\u0391\u0399\u0391 / \u039C\u03B7\u03C7\u03B1"
//                        + "\u03BD\u03BF\u03BB\u03CC\u03B3\u03BF\u03B9";
  
  AssertThat.document(filename)
            .hasSubject()
            .equalsTo(expectedSubject)
  ;
}

Unicode in XPath-Ausdrücken

In Kapitel 13.11: „XPath-Einsatz“ wird beschrieben, wie PDFUnit-Tests zusammen mit XPath funktionieren. Auch die XPath-Ausdrücke können Unicode enthalten. Im folgende Beispiel wird analysiert, ob irgendein XML-Knoten unterhalb von rsm:HeaderExchangedDocument das Zeichen mit dem Unicode \u20AC enthält:

@Test
public void hasZugferdData_ContainingEuroSign() throws Exception {
  String filename = "ZUGFeRD_1p0_COMFORT_Kraftfahrversicherung_Bruttopreise.pdf";
  String euroSign = "\u20AC";
  String noTextInHeader =
         "count(//rsm:HeaderExchangedDocument//text()[contains(., '%s')]) = 0";
  String noEuroSignInHeader = String.format(noTextInHeader, euroSign);
  XPathExpression exprNumberOfTradeItems = new XPathExpression(noEuroSignInHeader);
  AssertThat.document(filename)
            .hasZugferdData()
            .matchingXPath(exprNumberOfTradeItems)
  ;
}

UTF-8 File-Encoding für Shell-Skripte

Vorsicht bei Daten, die aus dem Dateisystem gelesen werden. Deren Interpretation ist vom Encoding des jeweiligen Dateisystems abhängig. Deshalb ist jedes Java-Programm, das Dateien verarbeitet, also auch PDFUnit, von der Umgebungsvariablen file.encoding abhängig.

Es gibt mehrere Möglichkeiten, diese Umgebungsvariable für den jeweiligen Java-Prozess zu setzen:

set _JAVA_OPTIONS=-Dfile.encoding=UTF8
set _JAVA_OPTIONS=-Dfile.encoding=UTF-8

java -Dfile.encoding=UTF8
java -Dfile.encoding=UTF-8

UTF-8 File-Encoding für ANT

Während der Entwicklung von PDFUnit gab es zwei Tests, die unter Eclipse fehlerfrei liefen, unter ANT aber mit einem Encoding-Fehler abbrachen. Die Ursache lag in der Java-System-Property file.encoding, die in der DOS-Box nicht auf UTF-8 stand.

Der folgende Befehl löste das Encoding-Problem unter ANT nicht:

// does not work for ANT:

ant -Dfile.encoding=UTF-8

Statt dessen wurde die Property so gesetzt, wie im vorhergehenden Abschnitt für Shell-Skripte beschrieben:

// Used when developing PDFUnit:

set JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8

Maven - UTF-8 Konfiguration in der pom.xml

In der pom.xml können Sie UTF-8 an vielen Stellen konfigurieren. Die folgenden Code-Ausschnitte zeigen mehrere Beispiele, wählen Sie die passenden für Ihr Problem:

<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>2.5.1</version>
  <configuration>
    <encoding>UTF-8</encoding>
  </configuration>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-resources-plugin</artifactId>
  <version>2.6</version>
  <configuration>
    <encoding>UTF-8</encoding>
  </configuration>
</plugin>

Eclipse auf UTF-8 einstellen

Wenn Sie XML-Dateien in Eclipse erstellen, ist es nicht unbedingt nötig, Eclipse auf UTF-8 einzurichten, denn XML-Dateien sind auf UTF-8 voreingestellt. Für andere Dateitypen ist aber die Codepage des Betriebssystems voreingestellt. Sie sollten daher, wenn Sie mit Unicode-Daten arbeiten, das Default-Encoding für den gesamten Workspace auf UTF-8 einstellen:

Abweichend von dieser Standardeinstellung können einzelne Dateien in einem anderen Encoding gepeichert werden.

Fehlermeldungen und Unicode

Wenn Tests fehlschlagen, die auf Unicode-Inhalte testen, kann es sein, dass Eclipse oder ein Browser die Fehlermeldung nicht ordentlich dargestellen. Ausschlaggebend dafür ist das File-Encoding der Ausgabe, das von PDFUnit selber nicht beeinflusst werden kann. Wenn Sie in Eclipse, ANT oder Maven dafür gesorgt haben, dass UTF-8 als Codepage verwendet wird, sind die meisten Probleme beseitigt. Danach können noch Zeichen aus der Codepage UTF-16 die Darstellung der Fehlermeldung korrumpieren.

Das PDF-Dokument im nächsten Beispiel enthält einen Layer-Namen, der UTF-16BE-Zeichen enthält. Um die Wirkung der Unicode-Zeichen in der Fehlermeldung zu zeigen, wurde der erwartet Layername bewusst falsch gewählt:

/**
 * The name of the layers consists of UTF-16BE and contains the 
 * byte order mark (BOM). The error message is not complete. 
 * It was corrupted by the internal Null-bytes.
 */
@Test
public void hasLayer_NameContainingUnicode_UTF16_ErrorIntended() throws Exception {
  String filename = "documentUnderTest.pdf";
  
//  String layername = "Ebene 1(4)"; // This is shown by Adobe Reader®,
//                     "Ebene _XXX"; // and this is the used string   
  String wrongNameWithUTF16BE = 
  "\u00fe\u00ff\u0000E\u0000b\u0000e\u0000n\u0000e\u0000 \u0000_XXX";

  AssertThat.document(filename)
            .hasLayer()
            .equalsTo(wrongNameWithUTF16BE);
  ;
}

Wenn die Tests mit ANT ausgeführt wurden, zeigt ein Browser die von PDFUnit erzeugte Fehlermeldung fehlerfrei an, einschließlich der Zeichenkette þÿEbene _XXX am Ende:

Eclipse dagegen hat in der JUnit-View Probleme mit den Null-Bytes. Die Meldung '...\unicode_layerName.pdf' does not contain a layer with the name 'þÿ' endet nicht mit dem Text þÿEbene _XXX. Sie wird nach der internen Byte-Order-Markierung abgeschnitten:

Unicode für unsichtbare Zeichen - &nbsp;

Im praktischen Betrieb trat einmal ein Problem auf, bei dem ein non-breaking space in den Testdaten enthalten war, das zunächst als normales Leerzeichen wahrgenommen wurde. Der String-Vergleich lieferte aber einen Fehler, der erst durch die Verwendung von Unicode beseitigt werden konnte:

@Test
public void nodeValueWithUnicodeValue() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  DefaultNamespace defaultNS = new DefaultNamespace("http://www.w3.org/1999/xhtml");
  String nodeValue = "The code ... the button's";
  String nodeValueWithNBSP = nodeValue + "\u00A0"; // The content terminates with a NBSP.
  XMLNode nodeP7 = new XMLNode("default:p[7]", nodeValueWithNBSP, defaultNS);

  AssertThat.document(filename)
            .hasXFAData()
            .withNode(nodeP7)
  ;
}