1. Die Sprache PDFLang

← Inhalt · Weiter: Typen des Dokuments →

PDFLang ist so entworfen, dass Menschen es lesen können, die keine Programme schreiben. Keine Klassen, keine Vererbung, keine Typdeklarationen, keine Semikolons. Ein Skript ist eine Sammlung von Prüfungen, fast in natürlicher Sprache geschrieben.


1.1 Aufbau eines Skripts

// Ein Kommentar beginnt mit zwei Schrägstrichen und reicht bis zum Zeilenende.

profile "profil-name" {           // profile ist optional: es benennt und
                                  // gruppiert das Ganze, und der Name
                                  // erscheint im Bericht.

  const GRENZE = 300%             // Konstanten: üblicherweise in Großbuchstaben

  check "Name der Prüfung" {      // jeder check wird ein Abschnitt im Bericht
    require doc.page_count > 0    // eine Prüfung
  }

  check "Weitere Prüfung" {       // beliebig viele checks
    require doc.title != ""
  }
}

profile darf entfallen — ein Skript kann auch nur eine Folge von checks sein:

check "Simple" {
  require doc.page_count > 0
}

Etiketten an checks

Etiketten dienen dazu, checks im Bericht zu ordnen und zu filtern:

check "Ink within limit" tags: ["prepress", "colors"] {
  require prepress::validate_tac_limits(300)
}

1.2 Zwei Arten zu prüfen

Jede Prüfung wird mit require oder assert geschrieben. Der einzige Unterschied ist die Meldung, die bei einem Fehlschlag im Bericht steht.

check "Comparing both forms" {

  // require: Die Meldung entsteht aus dem Ausdruck selbst.
  // Bei einem Fehlschlag zeigt der Bericht:
  //   "requirement not met: doc.page_count > 0"
  require doc.page_count > 0

  // assert: Sie schreiben die Meldung, die die empfangende Person liest.
  // Bei einem Fehlschlag erscheint sie unverändert:
  //   "PDF has no title in its metadata"
  assert doc.title != "", "PDF has no title in its metadata"
}

Faustregel: require, wenn der Ausdruck für sich spricht; assert, wenn die Person, die den Bericht liest, das Problem verstehen soll, ohne das Skript zu kennen.

Ein Fehlschlag hält die übrigen Prüfungen nicht auf

check "Three independent validations" {
  assert doc.page_count > 100, "too few pages"    // schlägt fehl
  assert doc.title != "", "no title"              // läuft trotzdem
  assert doc.author != "", "no author"            // diese auch
}

Der Bericht listet alle Probleme auf einmal. Das ist Absicht: Wer die Datei bekommt, will die vollständige Korrekturliste, nicht eine Korrektur nach der anderen.

Zwischen den checks gilt dasselbe — trifft ein check auf einen Laufzeitfehler (etwa eine unbekannte Variable), wird daraus eine Diagnose, und die übrigen checks laufen weiter.


1.3 Werte und Typen

Zahlen und Einheiten

check "Numbers" {
  x = 42          // ganze Zahl
  y = 2.5         // Dezimalzahl

  // Längeneinheiten werden in Punkt umgerechnet (1 pt = 1/72 Zoll):
  a = 3mm         // 8,5039... pt
  b = 2.5cm       // 70,866... pt
  c = 1in         // 72 pt
  d = 10pt        // 10 pt

  // Prozent behält den Zahlenwert:
  grenze = 300%   // 300

  require a < b            // alles in Punkt, direkt vergleichbar
  require c == 72.0
  require grenze == 300
}

3mm statt 8.504 schreiben zu können, ist genau der Punkt: Es liest sich natürlich für jemanden, der in Millimetern denkt, und die Umrechnung geht nicht daneben.

Text

check "Strings" {
  einfach = "einfacher Text"

  // Interpolation: #{...} setzt den Wert eines beliebigen Ausdrucks ein
  name = "dokument.pdf"
  meldung = "Analyzing #{name} with #{doc.page_count} pages"

  // Escapes: \n (Zeilenumbruch), \t (Tabulator), \" (Anführungszeichen), \\ (Backslash)
  zitat = "er sagte \"hallo\""

  // Ein unbekannter Backslash bleibt erhalten — dadurch lassen sich reguläre
  // Ausdrücke ohne doppeltes Escapen schreiben:
  muster = "\d{3}\.\d{3}\.\d{3}-\d{2}"

  require meldung.contains("pages")
}

Wahrheitswerte und was „wahr“ ist

check "True and false" {
  ja = true
  nein = false

  // Nur false und null sind falsch. Alles andere ist wahr —
  // auch 0, die leere Zeichenkette und die leere Liste.
  require 0        // besteht (0 ist wahr)
  require ""       // besteht (die leere Zeichenkette ist wahr)

  // Um Inhalt zu prüfen, vergleichen Sie also ausdrücklich:
  require doc.title != ""              // richtig
  require doc.pages.length > 0         // richtig
}

Nützlich bei Funktionen, die null zurückgeben:

check "Taking advantage of null" {
  beschreibung = data::lookup_value("batches.csv", "L2026-08")
  // null ist falsch, deshalb geht das direkt:
  assert beschreibung, "batch not found in the table"
}

Listen

check "Lists" {
  zahlen = [1, 2, 3]
  woerter = ["a", "b", "c"]
  gemischt = [1, "zwei", true]

  require zahlen.length == 3
  require zahlen.contains(2)
  require woerter.join(", ") == "a, b, c"

  // Der Zugriff beginnt bei 1: das erste Element ist das 1.
  require zahlen.get(1) == 1
  require zahlen.first() == 1
  require zahlen.last() == 3
}

1.4 Operatoren

check "Operators" {
  // Vergleich
  require 10 > 5
  require 10 >= 10
  require 3 < 4
  require 3 <= 3
  require "a" == "a"
  require "a" != "b"

  // Arithmetik
  require 2 + 3 == 5
  require 10 - 4 == 6
  require 3 * 4 == 12
  require 10 / 4 == 2.5        // geht nicht auf: Ergebnis ist dezimal
  require 10 / 5 == 2          // geht auf: bleibt ganzzahlig

  // Logik (Kurzschlussauswertung: rechts wird nur bei Bedarf ausgewertet)
  require true && true
  require false || true
  require !false

  // Praktischer Nutzen des Kurzschlusses: ohne Seiten wird rechts nie
  // ausgewertet, ein leeres Dokument erzeugt also keinen Fehler.
  require doc.page_count == 0 || doc.pages.first().width > 0
}

1.5 Blöcke: für jedes Element wiederholen

Ein Block ist Code in geschweiften Klammern, mit den Parametern zwischen zwei senkrechten Strichen. Es liest sich als „für jede Seite tue …“.

check "Walking through pages" {

  // each: führt den Block für jedes Element aus
  doc.pages.each { |page|
    assert page.width > 0, "page #{page.number} has no width"
  }

  // each_with_index: gibt zusätzlich die Position (0, 1, 2 …)
  doc.fonts.each_with_index { |font, i|
    print("font", i, ":", font.name)
  }

  // all: wahr, wenn alle Elemente die Bedingung erfüllen
  require doc.fonts.all { |f| f.is_embedded }

  // any: wahr, wenn mindestens eines sie erfüllt
  require doc.pages.any { |p| p.extract_text() != "" }

  // filter: behält nur die Elemente, die sie erfüllen
  leere = doc.pages.filter { |p| p.extract_text() == "" }
  assert leere.length == 0, "#{leere.length} blank page(s)"

  // map: verwandelt jedes Element in eine neue Liste
  namen = doc.fonts.map { |f| f.name }
  print("fonts in use:", namen.join(", "))
}

Blöcke lassen sich verketten — aber in derselben Zeile: kein Zeilenumbruch vor dem Punkt.

check "Chaining" {
  // Nicht eingebettete Schriften, nur die Namen, mit Komma verbunden
  probleme = doc.fonts.filter { |f| !f.is_embedded }.map { |f| f.name }
  assert probleme.length == 0,
    "fonts not embedded: #{probleme.join(", ")}"
}

Wird die Zeile zu lang, teilen Sie sie in benannte Schritte, statt die Kette zu brechen — das liest sich ohnehin besser:

check "Named steps" {
  lose = doc.fonts.filter { |f| !f.is_embedded }
  namen = lose.map { |f| f.name }
  assert namen.length == 0, "fonts not embedded: #{namen.join(", ")}"
}

1.6 Funktionen: einer Regel einen Namen geben

Wenn dieselbe Prüfung mehrfach auftaucht, geben Sie ihr einen Namen:

// Der Wert einer Funktion ist der ihres letzten Ausdrucks — kein return.
function ist_a4(page) {
  abs(page.width - 595.0) < 5.0 && abs(page.height - 842.0) < 5.0
}

function zu_viel_farbe(page, grenze) {
  page.tac > grenze
}

check "Format and ink" {
  // So liest sich der check fast wie ein Satz
  require doc.pages.all { |p| ist_a4(p) }

  doc.pages.each { |page|
    assert !zu_viel_farbe(page, 300), "page #{page.number} has too much ink"
  }
}

Regeln für Funktionen:

  • Parameter gelten nur innerhalb der Funktion.
  • Eine Funktion darf andere aufrufen.
  • Rekursion ist erlaubt, begrenzt auf 200 Aufrufe (damit ein außer Kontrolle geratenes Skript den Prozess nicht blockiert).

1.7 import: zwischen Profilen wiederverwenden

Legen Sie gemeinsame Regeln in eine Datei und importieren Sie sie dort, wo Sie sie brauchen.

bibliothek.pdfl:

// Vom Team gemeinsam genutzte Konstanten und Funktionen
const OFFSET_TAC = 300%
const STANDARD_ANSCHNITT = 3mm

function a4_seite(page) {
  abs(page.width - 595.0) < 5.0 && abs(page.height - 842.0) < 5.0
}

magazin.pdfl:

// Der Pfad ist relativ zu DIESER Datei
import "bibliothek.pdfl"

check "Format" {
  // OFFSET_TAC und a4_seite stammen aus dem Import
  require doc.pages.all { |p| a4_seite(p) }
  require prepress::validate_tac_limits(OFFSET_TAC)
}

Dieselbe Datei wird nur einmal geladen, auch wenn mehrere Skripte sie importieren — zyklische Importe blockieren also nichts.


1.8 rule: Seite für Seite prüfen

Eine rule ist ein check, der einmal pro Seite läuft, wobei die Seite bereits an die Variable page gebunden ist:

// Ohne "on": läuft auf allen Seiten
rule "Every page has text" {
  assert page.extract_text().trim() != "",
    "page #{page.number} is blank"
}

Mit on wählen Sie die betroffenen Seiten:

rule "Body pages numbered" on doc.pages.filter { |p| p.number > 2 } {
  fuss = region(0, 0, page.width, 60)
  assert text::extract_from_region(page.number, fuss) != "",
    "page #{page.number} has no page number in the footer"
}

Zur Syntax: Endet der Ausdruck nach on mit einer Eigenschaft (etwa on doc.pages), setzen Sie ihn in Klammern; sonst würde die geschweifte Klammer des Rumpfes als Block dieses Aufrufs gelesen:

rule "Example" on (doc.pages) {     // Klammern nötig
  require page.width > 0
}

1.9 Variablen und Gültigkeitsbereich

const GLOBAL = 100          // in der ganzen Datei sichtbar

check "Scope" {
  lokal = 42                // nur in diesem check sichtbar

  doc.pages.each { |page|
    innen = page.width      // nur in diesem Block sichtbar
    require innen > 0
  }

  require lokal == 42       // weiterhin sichtbar
  require GLOBAL == 100     // weiterhin sichtbar
}

Üblich sind Großbuchstaben für Konstanten und Kleinbuchstaben für Variablen. Die Sprache erzwingt das nicht, aber die Beispiele und die mitgelieferten Profile halten sich daran.


1.10 Meldungen, die dem Empfänger helfen

Die Qualität des Berichts hängt an den Meldungen, die Sie schreiben. Vergleichen Sie:

check "Poor messages" {
  require doc.pages.all { |p| p.tac <= 300 }
  // Bericht: "requirement not met: doc.pages.all() { ... }"
  // — die empfangende Person weiß weder welche Seite noch um wie viel.
}

check "Good messages" {
  doc.pages.each { |page|
    assert page.tac <= 300,
      "Page #{page.number}: ink coverage #{page.tac}% (max 300%)"
  }
  // Bericht: "Page 7: ink coverage 324% (max 300%)"
  // — die Bedienperson weiß sofort, was zu ändern ist.
}

Für Zusatzinformationen, die keine Fehler sind, nehmen Sie print(). Die Ausgabe geht auf die Fehlerausgabe und verschmutzt den Bericht nicht:

check "Context" {
  print("Analyzing", doc.page_count, "pages")
  print("Fonts:", prepress::list_fonts().join(", "))
  require doc.page_count > 0
}

1.11 Häufige Fehler

Meldung Ursache Abhilfe
expected end of line after statement Zwei Anweisungen in einer Zeile Eine Anweisung pro Zeile
unknown variable: x Vor der Zuweisung benutzt oder außerhalb des Bereichs Auf derselben Ebene deklarieren
unknown function: text::xyz Falscher Name oder Funktion existiert nicht Kapitel des Namensraums nachschlagen
fix:: is only available in the 'pdfl fix' command fix:: unter pdfl run benutzt pdfl fix input.pdf script.pdfl --output out.pdf
unknown unit: 'kg' Ungültige Einheit pt, mm, cm, in oder % verwenden
expected '{' with the rule body Ausdruck nach on endet mit einer Eigenschaft In Klammern setzen
unexpected expression: Dot Kette durch Zeilenumbruch getrennt .methode in derselben Zeile lassen oder Zwischenvariable nutzen

Vor dem Ausführen lohnen sich diese beiden Befehle immer:

pdfl lint mein_profil.pdfl    # ungenutzte Variablen, doppelte checks …
pdfl fmt mein_profil.pdfl     # einheitliche Formatierung

← Inhalt · Weiter: Typen des Dokuments →