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
onmit einer Eigenschaft (etwaon 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