1. Le langage PDFLang
← Sommaire · Suivant : types du document →
PDFLang est conçu pour être lu par des gens qui n'écrivent pas de programmes. Pas de classes, pas d'héritage, pas de déclarations de types, pas de points-virgules. Un script est un ensemble de vérifications écrites presque en langue naturelle.
1.1 Structure d'un script
// Un commentaire commence par deux barres obliques et va jusqu'au bout de la ligne.
profile "nom-du-profil" { // profile est facultatif : il nomme et
// regroupe l'ensemble, et son nom apparaît
// dans le rapport.
const LIMITE = 300% // constantes : par convention en majuscules
check "Nom du contrôle" { // chaque check devient une section du rapport
require doc.page_count > 0 // une validation
}
check "Autre contrôle" { // autant de checks que nécessaire
require doc.title != ""
}
}
profile peut être omis — un script peut n'être qu'une suite de checks :
check "Simple" {
require doc.page_count > 0
}
Étiquettes sur les checks
Les étiquettes servent à classer et filtrer les checks dans le rapport :
check "Ink within limit" tags: ["prepress", "colors"] {
require prepress::validate_tac_limits(300)
}
1.2 Deux façons de valider
Toute validation s'écrit avec require ou assert. La seule différence est le
message qui apparaît dans le rapport en cas d'échec.
check "Comparing both forms" {
// require : le message est fabriqué à partir de l'expression elle-même.
// En cas d'échec, le rapport affiche :
// "requirement not met: doc.page_count > 0"
require doc.page_count > 0
// assert : c'est vous qui écrivez le message que lira le destinataire.
// En cas d'échec, il apparaît tel quel :
// "PDF has no title in its metadata"
assert doc.title != "", "PDF has no title in its metadata"
}
Règle pratique : require quand l'expression se lit toute seule ; assert
quand la personne qui lira le rapport doit comprendre le problème sans connaître
le script.
Un échec n'arrête pas les autres contrôles
check "Three independent validations" {
assert doc.page_count > 100, "too few pages" // échoue
assert doc.title != "", "no title" // s'exécute quand même
assert doc.author != "", "no author" // celle-ci aussi
}
Le rapport liste tous les problèmes d'un coup. C'est volontaire : la personne qui reçoit le fichier veut la liste complète des corrections, pas une correction à la fois.
Il en va de même entre les checks — si un check rencontre une erreur d'exécution (une variable inconnue, par exemple), elle devient un diagnostic et les autres checks continuent.
1.3 Valeurs et types
Nombres et unités
check "Numbers" {
x = 42 // entier
y = 2.5 // décimal
// Les unités de longueur sont converties en points (1 pt = 1/72 pouce) :
a = 3mm // 8,5039... pt
b = 2.5cm // 70,866... pt
c = 1in // 72 pt
d = 10pt // 10 pt
// Le pourcentage garde la valeur telle quelle :
limite = 300% // 300
require a < b // tout est en points, la comparaison est directe
require c == 72.0
require limite == 300
}
Pouvoir écrire 3mm au lieu de 8.504 est précisément l'intérêt : cela se lit
naturellement pour qui pense en millimètres, et la conversion ne se trompe pas.
Texte
check "Strings" {
simple = "texte simple"
// Interpolation : #{...} insère la valeur de n'importe quelle expression
nom = "document.pdf"
message = "Analyzing #{nom} with #{doc.page_count} pages"
// Échappements : \n (saut de ligne), \t (tabulation), \" (guillemet), \\ (barre)
cite = "il a dit \"bonjour\""
// Une barre oblique inverse inconnue est conservée telle quelle — c'est ce qui
// permet d'écrire des expressions régulières sans double échappement :
motif = "\d{3}\.\d{3}\.\d{3}-\d{2}"
require message.contains("pages")
}
Booléens et ce qui est « vrai »
check "True and false" {
oui = true
non = false
// Seuls false et null sont faux. Tout le reste est vrai —
// y compris 0, la chaîne vide et la liste vide.
require 0 // passe (0 est vrai)
require "" // passe (la chaîne vide est vraie)
// Donc pour vérifier un contenu, comparez explicitement :
require doc.title != "" // correct
require doc.pages.length > 0 // correct
}
C'est utile avec les fonctions qui retournent null :
check "Taking advantage of null" {
description = data::lookup_value("batches.csv", "L2026-08")
// null est faux, on peut donc écrire directement :
assert description, "batch not found in the table"
}
Listes
check "Lists" {
nombres = [1, 2, 3]
mots = ["a", "b", "c"]
melange = [1, "deux", true]
require nombres.length == 3
require nombres.contains(2)
require mots.join(", ") == "a, b, c"
// L'accès commence à 1 : le premier élément est le 1er
require nombres.get(1) == 1
require nombres.first() == 1
require nombres.last() == 3
}
1.4 Opérateurs
check "Operators" {
// Comparaison
require 10 > 5
require 10 >= 10
require 3 < 4
require 3 <= 3
require "a" == "a"
require "a" != "b"
// Arithmétique
require 2 + 3 == 5
require 10 - 4 == 6
require 3 * 4 == 12
require 10 / 4 == 2.5 // division non entière : résultat décimal
require 10 / 5 == 2 // division exacte : reste entier
// Logique (évaluation court-circuit : la droite n'est évaluée qu'au besoin)
require true && true
require false || true
require !false
// Usage concret du court-circuit : sans pages, la droite n'est jamais
// évaluée et un document vide ne provoque pas d'erreur.
require doc.page_count == 0 || doc.pages.first().width > 0
}
1.5 Les blocs : répéter pour chaque élément
Un bloc est du code entre accolades, avec ses paramètres entre deux barres verticales. Cela se lit « pour chaque page, faire… ».
check "Walking through pages" {
// each : exécute le bloc pour chaque élément
doc.pages.each { |page|
assert page.width > 0, "page #{page.number} has no width"
}
// each_with_index : donne aussi la position (0, 1, 2…)
doc.fonts.each_with_index { |font, i|
print("font", i, ":", font.name)
}
// all : vrai si tous les éléments satisfont la condition
require doc.fonts.all { |f| f.is_embedded }
// any : vrai si au moins un élément la satisfait
require doc.pages.any { |p| p.extract_text() != "" }
// filter : ne garde que les éléments qui satisfont la condition
vides = doc.pages.filter { |p| p.extract_text() == "" }
assert vides.length == 0, "#{vides.length} blank page(s)"
// map : transforme chaque élément en une nouvelle liste
noms = doc.fonts.map { |f| f.name }
print("fonts in use:", noms.join(", "))
}
Les blocs s'enchaînent — mais sur la même ligne : pas de retour à la ligne avant le point.
check "Chaining" {
// Polices non incorporées, seulement les noms, réunis par des virgules
problemes = doc.fonts.filter { |f| !f.is_embedded }.map { |f| f.name }
assert problemes.length == 0,
"fonts not embedded: #{problemes.join(", ")}"
}
Si la ligne devient trop longue, coupez-la en étapes nommées plutôt que de briser l'enchaînement — c'est de toute façon plus lisible :
check "Named steps" {
libres = doc.fonts.filter { |f| !f.is_embedded }
noms = libres.map { |f| f.name }
assert noms.length == 0, "fonts not embedded: #{noms.join(", ")}"
}
1.6 Les fonctions : donner un nom à une règle
Quand la même validation revient plusieurs fois, donnez-lui un nom :
// La valeur d'une fonction est celle de sa dernière expression — pas de return.
function est_a4(page) {
abs(page.width - 595.0) < 5.0 && abs(page.height - 842.0) < 5.0
}
function trop_encre(page, limite) {
page.tac > limite
}
check "Format and ink" {
// Le check se lit alors presque comme une phrase
require doc.pages.all { |p| est_a4(p) }
doc.pages.each { |page|
assert !trop_encre(page, 300), "page #{page.number} has too much ink"
}
}
Règles des fonctions :
- Les paramètres n'existent qu'à l'intérieur de la fonction.
- Une fonction peut en appeler d'autres.
- La récursion est permise, plafonnée à 200 appels (pour qu'un script emballé ne bloque pas le processus).
1.7 import : réutiliser entre profils
Mettez les règles communes dans un fichier et importez-le où vous en avez besoin.
bibliotheque.pdfl :
// Constantes et fonctions partagées par l'équipe
const TAC_OFFSET = 300%
const FOND_PERDU = 3mm
function page_a4(page) {
abs(page.width - 595.0) < 5.0 && abs(page.height - 842.0) < 5.0
}
magazine.pdfl :
// Le chemin est relatif à CE fichier
import "bibliotheque.pdfl"
check "Format" {
// TAC_OFFSET et page_a4 viennent de l'import
require doc.pages.all { |p| page_a4(p) }
require prepress::validate_tac_limits(TAC_OFFSET)
}
Un même fichier n'est chargé qu'une seule fois, même si plusieurs scripts l'importent — les imports circulaires ne bloquent donc rien.
1.8 rule : valider page par page
Une rule est un check exécuté une fois par page, la page étant déjà liée à la
variable page :
// Sans "on" : s'exécute sur toutes les pages
rule "Every page has text" {
assert page.extract_text().trim() != "",
"page #{page.number} is blank"
}
Avec on, vous choisissez les pages concernées :
rule "Body pages numbered" on doc.pages.filter { |p| p.number > 2 } {
pied = region(0, 0, page.width, 60)
assert text::extract_from_region(page.number, pied) != "",
"page #{page.number} has no page number in the footer"
}
Point de syntaxe : si l'expression après
onse termine par une propriété (commeon doc.pages), mettez-la entre parenthèses ; sinon l'accolade du corps serait prise pour un bloc de cet appel :rule "Example" on (doc.pages) { // parenthèses nécessaires require page.width > 0 }
1.9 Variables et portée
const GLOBAL = 100 // visible dans tout le fichier
check "Scope" {
locale = 42 // visible seulement dans ce check
doc.pages.each { |page|
interne = page.width // visible seulement dans ce bloc
require interne > 0
}
require locale == 42 // toujours visible
require GLOBAL == 100 // toujours visible
}
L'usage veut des majuscules pour les constantes et des minuscules pour les variables. Le langage ne l'impose pas, mais les exemples et les profils fournis suivent cette convention.
1.10 Des messages utiles à qui reçoit le fichier
La qualité du rapport tient aux messages que vous écrivez. Comparez :
check "Poor messages" {
require doc.pages.all { |p| p.tac <= 300 }
// Rapport : "requirement not met: doc.pages.all() { ... }"
// — le destinataire ne sait ni quelle page ni de combien.
}
check "Good messages" {
doc.pages.each { |page|
assert page.tac <= 300,
"Page #{page.number}: ink coverage #{page.tac}% (max 300%)"
}
// Rapport : "Page 7: ink coverage 324% (max 300%)"
// — l'opérateur sait immédiatement quoi corriger.
}
Pour les informations complémentaires qui ne sont pas des erreurs, utilisez
print(). Sa sortie va sur la sortie d'erreur et ne pollue pas le rapport :
check "Context" {
print("Analyzing", doc.page_count, "pages")
print("Fonts:", prepress::list_fonts().join(", "))
require doc.page_count > 0
}
1.11 Erreurs courantes
| Message | Cause | Correction |
|---|---|---|
expected end of line after statement |
Deux instructions sur une ligne | Une instruction par ligne |
unknown variable: x |
Utilisée avant l'affectation, ou hors de portée | Déclarez-la au même niveau |
unknown function: text::xyz |
Nom erroné ou fonction inexistante | Voyez le chapitre de l'espace de noms |
fix:: is only available in the 'pdfl fix' command |
fix:: employé avec pdfl run |
pdfl fix input.pdf script.pdfl --output out.pdf |
unknown unit: 'kg' |
Unité invalide | Utilisez pt, mm, cm, in ou % |
expected '{' with the rule body |
L'expression après on finit par une propriété |
Mettez-la entre parenthèses |
unexpected expression: Dot |
Enchaînement coupé par un retour à la ligne | Gardez .methode sur la même ligne, ou passez par une variable |
Avant d'exécuter, ces deux commandes valent toujours la peine :
pdfl lint mon_profil.pdfl # variables inutilisées, checks en double…
pdfl fmt mon_profil.pdfl # mise en forme uniforme