1. Die Sprache PDFLang
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
Abschnitt betitelt „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
Abschnitt betitelt „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
Abschnitt betitelt „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
Abschnitt betitelt „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
Abschnitt betitelt „1.3 Werte und Typen“Zahlen und Einheiten
Abschnitt betitelt „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.
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
Abschnitt betitelt „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"}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
Abschnitt betitelt „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
Abschnitt betitelt „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
Abschnitt betitelt „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
Abschnitt betitelt „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 Funktionenconst 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 Dateiimport "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
Abschnitt betitelt „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 Seitenrule "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ötigrequire page.width > 0}
1.9 Variablen und Gültigkeitsbereich
Abschnitt betitelt „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
Abschnitt betitelt „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
Abschnitt betitelt „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