1. Le langage PDFLang
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
Section intitulée « 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
Section intitulée « É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
Section intitulée « 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
Section intitulée « 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
Section intitulée « 1.3 Valeurs et types »Nombres et unités
Section intitulée « 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.
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 »
Section intitulée « 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"}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
Section intitulée « 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
Section intitulée « 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
Section intitulée « 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
Section intitulée « 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'équipeconst 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 fichierimport "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
Section intitulée « 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 pagesrule "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écessairesrequire page.width > 0}
1.9 Variables et portée
Section intitulée « 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
Section intitulée « 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
Section intitulée « 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