3.28. Texte

Überblick

Der häufigste Testfall für PDF-Dokumente ist vermutlich, die Existenz erwarteter Texte zu überprüfen. Dafür stehen vielfältige Methoden zur Verfügung:

// Testing page content:
.hasText()      // pages has to be specified before

// Validating expected text:
.hasText().containing(..) 
.hasText().containing(.., WhitespaceProcessing)    1
.hasText().endingWith(..)
.hasText().endingWith(.., WhitespaceProcessing)
.hasText().equalsTo(..) 
.hasText().equalsTo(.., WhitespaceProcessing)
.hasText().matchingRegex(..) 
.hasText().startingWith(..) 

// Prove the absence of defined text:
.hasText().notContaining(..) 
.hasText().notContaining(.., WhitespaceProcessing)
.hasText().notEndingWith(..)
.hasText().notMatchingRegex(..) 
.hasText().notStartingWith(..)

// Validate multiple text in an expected order:
.hasText().inOrder(..)
.hasText().containingFirst(..).then(..)

// Comparing visible text with ZUGFeRD data:
.hasText.containingZugferdData(..)                 2

1

Das Kapitel 13.5: „Behandlung von Whitespaces“ beschreibt die unterschiedlichen Möglichkeiten, mit Whitespaces umzugehen.

2

Das Kapitel 3.39: „ZUGFeRD“ beschreibt, wie der sichtbare Inhalt von PDF-Dokumenten mit den Inhalten der unsichtbaren ZUGFeRD-Daten verglichen werden kann.

Text auf bestimmten Seiten

Wenn Sie einen bestimmten Text auf der ersten Seite eines Anschreibens suchen, sieht ein Test folgendermaßen aus:

@Test
public void hasText_OnFirstPage() throws Exception {
  String filename = "documentUnderTest.pdf";

  AssertThat.document(filename)
            .restrictedTo(FIRST_PAGE)
            .hasText() 
            .containing("Content on first page.") 
  ;
}

Ein Text auf der letzten Seite wird folgendermaßen überprüft:

@Test
public void hasText_OnLastPage() throws Exception {
  String filename = "documentUnderTest.pdf";

  AssertThat.document(filename)
            .restrictedTo(LAST_PAGE)
            .hasText() 
            .containing("Content on last page.")
  ;
}

Auch Tests mit beliebigen individuellen Seiten sind möglich:

@Test
public void hasText_OnIndividualPages() throws Exception {
  String filename = "documentUnderTest.pdf";
  PagesToUse pages23 = PagesToUse.getPages(2, 3);  1

  AssertThat.document(filename)
            .restrictedTo(pages23)
            .hasText() 
            .containing("Content on")
  ;
}

1

Mit der Methode getPages(Integer[]) können beliebige Seitenkombinationen definiert werden. Für eine einzelne Seite kann auch der Singular PagesToUse.getPage(int) benutzt werden.

Für typische Seiten stehen Konstanten zur Verfügung, u.a. FIRST_PAGE, LAST_PAGE, EVEN_PAGES und ODD_PAGES. Das Kapitel 13.2: „Seitenauswahl“ beschreibt die Seitenauswahl ausführlich.

Text auf allen Seiten

Für Prüfungen, die sich auf alle Seiten beziehen, stehen drei weitere Konstanten zur Verfügung: ANY_PAGE, EACH_PAGE und EVERY_PAGE. Die letzten beiden sind funktional identisch und existieren nur aus sprachlichen Gründen doppelt.

@Test 
public void hasText_OnEveryPage() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  AssertThat.document(filename)
            .retricted(EVERY_PAGE)
            .hasText()
            .startingWith("PDFUnit")
  ;
}
@Test  
public void hasText_OnAnyPage() throws Exception {
  String filename = "documentUnderTest.pdf";

  AssertThat.document(filename)
            .restrictedTo(ANY_PAGE)
            .hasText() 
            .containing("Page # 3") 
  ;
}

Die Konstanten EVERY_PAGE und EACH_PAGE fordern, dass der zu suchende Text wirklich auf jeder Seite existiert. Mit der Konstanten ANY_PAGE reicht es, wenn der erwartete Text auf irgendeiner Seite des Dokumentes vorkommt.

Text in Seitenausschnitten

Text kann aber nicht nur auf vollständigen Seite gesucht werden, sondern auch in Seitenausschnitten. Das Kapitel 3.30: „Texte - in Seitenausschnitten“ beschreibt diesen Aspekt ausführlich.

Fließende Seitenangaben mit Unter- und Obergrenze

Es kann den Wunsch geben, Texte auf jeder Seite zu überprüfen, aber nicht auf der ersten Seite. Ein solcher Test sieht folgendermaßen aus:

@Test
public void hasText_OnAllPagesAfter3() throws Exception {
  String filename = "documentUnderTest.pdf";
  PagesToUse pagesAfter3 = ON_EVERY_PAGE.after(3);   1

  AssertThat.document(filename)
            .restrictedTo(pagesAfter3)
            .hasText() 
            .containing("Content")
  ;
}

1

Der Wert '3' ist eine exclusive Untergrenze. Die Validierung beginnt mit Seite 4.

Die Zählung der Seitenzahlen beginnt mit 1.

Ungültige Seitenobergrenzen sind nicht unbedingt ein Fehler. Im folgenden Beispiel wird Text auf irgendeiner Seite zwischen 1 und 99 gesucht. Obwohl das Dokument nur 4 Seiten hat, endet der Test erfolgreich, weil die gesuchte Zeichenkette auf Seite 1 gefunden wird:

/**
 * Attention: The document has the search token on page 1. 
 * And '1' is before '99'. So, this test ends successfully.
 */
@Test
public void hasText_OnAnyPageBefore_WrongUpperLimit() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  AssertThat.document(filename)
            .restrictedTo(AnyPage.before(99))
            .hasText()
            .containing("Content on")
  ;
}

Seitenübergreifende Textvergleiche

Im folgenden Beispiel wird ein Text gesucht, der sich über 2 Seiten erstreckt. Diese 2 Seiten müssen nicht volle Seiten sein, sondern können - wie im Beispiel - auch Seitenausschnitte sein. Auf dieses Weise kann Fließtext ohne Header und Footer analysiert werden:


@Test
public void hasText_SpanningOver2Pages() throws Exception {
  String filename = "documentUnderTest.pdf";
  String textOnPage1 = "Text starts  on page 1 and ";
  String textOnPage2 = "continues on page 2";
  String expectedText = textOnPage1 + textOnPage2;
  PagesToUse pages1to2 = PagesToUse.spanningFrom(1).to(2);
  
  // Define the section without header and footer:
  int leftX  = 18;
  int upperY = 30;
  int width  = 182;
  int height = 238;
  PageRegion regionWithoutHeaderAndFooter = new PageRegion(leftX, upperY, width, height);
  
  AssertThat.document(filename)
            .restrictedTo(pages1to2)
            .restrictedTo(regionWithoutHeaderAndFooter)
            .hasText() 
            .containing(expectedText)
  ;
}

Nach der Methode .hasText() stehen alle oben genannte Vergleichsmethoden zur Verfügung.

Verneinte Suche

Auch die Abwesenheit von Text kann ein wichtiges Testziel sein, vor allem wenn es auf Teile einer Seite beschränkt wird. Die Tests dazu entsprechen der allgemeinen Umgangssprache:

@Test
public void hasText_NotMatchingRegex() throws Exception {
  String filename = "documentUnderTest.pdf";
    
  PagesToUse page2 = PagesToUse.getPage(2);
  PageRegion region = new PageRegion(70, 80, 90, 60);
  AssertThat.document(filename)
            .restrictedTo(page2)
            .restrictedTo(region)
            .hasNoText()
  ;
}

Zeilenumbrüche im Text

Zeilenumbrüche im Text werden beim Vergleich normalisiert, sowohl Zeilenumbrüche im Text der PDF-Seite, als auch die im Suchstring. Im folgenden Beispiel stammt der zu suchende Text aus dem Dokument Digital Signatures for PDF Documents von Bruno Lowagie (iText Software). Der erste Absatz sieht optisch so aus:

Tests auf den markierten Text ohne Berücksichtigung auf Zeilenumbrüche sehen folgendermaßen aus. Beide laufen erfolgreich durch, weil Whitespaces normalisiert werden.

/**
 * The expected search string does not contain a line break. 
 */
@Test
public void hasText_LineBreakInPDF() throws Exception {
  String filename = "digitalsignatures20121017.pdf";
  String text = "The technology was conceived";

  AssertThat.document(filename)
            .restrictedTo(FIRST_PAGE)
            .hasText() 
            .containing(text)
  ;
}
/**
 * The expected search string intentionally contains other line breaks. 
 */
@Test
public void hasText_LineBreakInExpectedString() throws Exception {
  String filename = "digitalsignatures20121017.pdf";
  String text = "The " +
                "\n " +
                "technology " +
                "\n " +
                "was " +
                "\n " +
                "conceived";
                
  AssertThat.document(filename)
            .restrictedTo(FIRST_PAGE)
            .hasText() 
            .containing(text)
  ;
}

Sollte eine Normalisierung der Whitespaces nicht erwünscht sein, so kann bei den meisten Textvergleichsmethoden noch ein zweiter Parameter übergeben werden, der die Art der Whitespace-Behandlung steuert. Folgende Möglichkeiten gibt es:

// Constants to define whitespace processing:
WhitespaceProcessing.IGNORE
WhitespaceProcessing.NORMALIZE
WhitespaceProcessing.KEEP

Keine leeren Seiten

Sie können auch überprüfen, dass Ihr PDF-Dokument keine leere Seiten enthält:

@Test
public void hasText_AnyPageEmpty() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  AssertThat.document(filename)
            .hasText()
  ;
}

Mehrere Suchbegriffe gleichzeitig

Wenn auf einer Seite mehrere Texte gesucht werden, ist es lästig, für jeden Suchbegriff einen eigenen Funktionsaufruf zu schreiben. Deshalb können die Funktionen containing(..) und notContaining(..) mit mehreren Suchbegriffen aufgerufen werden:

@Test
public void hasText_Containing_MultipleTokens() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  AssertThat.document(filename)
            .restrictedTo(ODD_PAGES)
            .hasText() 
            .containing("on", "page", "odd pagenumber") // multiple search tokens
  ;
}
@Test
public void hasText_NotContaining_MultipleTokens() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  AssertThat.document(filename)
            .restrictedTo(FIRST_PAGE)
            .hasText() 
            .notContaining("even pagenumber", "Page #2") 
  ;
}

Die Tests sind erfolgreich, wenn im ersten Beispiel alle Suchbegriffe gefunden werden, oder im zweiten Beispiel eben alle nicht.

Verkettung von Textvergleichen

Textvergleiche können verkettet werden, sie beziehen sich dann jeweils auf die zuvor spezifizierten Seiten:

@Test
public void hasText_MultipleInvocation() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  AssertThat.document(filename)
            .restrictedTo(ANY_PAGE)
            .hasText() 
            .startingWith("PDFUnit")
            .containing("Content on last page.")
            .matchingRegex(".*[Cc]ontent.*")  
            .endingWith("of 4")
  ;
}

Potentielles Problem mit Fließtext

Die sichtbare Reihenfolge des Textes einer PDF-Seite entspricht nicht zwingend der Textreihenfolge innerhalb des PDF-Dokumentes. Im folgenden Screenshot ist der umrahmte Text ein eigenes Textobjekt, das nicht zum 'normalen' Fließtext der Seite gehört. Deshalb funktioniert auch der nachfolgende Test.

@Test
public void hasText_TextNotInVisibleOrder() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  String firstAndLastLine = "Content at the beginning. Content at the end.";
                           
  AssertThat.document(filename)
            .restrictedTo(FIRST_PAGE)
            .hasText()
            .containing(firstAndLastLine) 
             
  ;
}

Wenn Sie sich den Rahmen wegdenken, könnte der Eindruck entstehen, dass der Text im Rahmen Teil des Fließtextes wäre. Ein Test mit einem Erwartungswert, der diesem vermeintlichen Fließtext entspricht, würde fehlschlagen.