1. El lenguaje PDFLang
← Índice · Siguiente: Tipos del documento →
PDFLang se diseñó para que la lea quien no programa. No hay clases, herencia, tipos declarados ni punto y coma. Un script es una lista de comprobaciones escritas casi en español.
1.1 La estructura de un script
// Los comentarios empiezan con dos barras y llegan hasta el fin de la línea.
profile "nombre-del-perfil" { // profile es opcional: agrupa y da nombre al
// conjunto; el nombre aparece en el informe.
const LIMITE = 300% // constantes: por convención en MAYÚSCULAS
check "Nombre del check" { // cada check se vuelve una sección del informe
require doc.page_count > 0 // una validación
}
check "Otro check" { // tantos checks como quieras
require doc.title != ""
}
}
El profile es opcional: un script puede tener solo checks sueltos:
check "Simple" {
require doc.page_count > 0
}
Etiquetas en los checks
Las etiquetas sirven para organizar y filtrar visualmente los checks en el informe:
check "TAC dentro del límite" tags: ["prepress", "colores"] {
require prepress::validate_tac_limits(300)
}
1.2 Las dos formas de validar
Toda validación usa require o assert. La única diferencia es el mensaje que
aparece en el informe cuando la validación falla.
check "Comparando las dos formas" {
// require: el mensaje se genera a partir de la propia expresión.
// Si falla, el informe muestra:
// "requirement not met: doc.page_count > 0"
require doc.page_count > 0
// assert: tú escribes el mensaje que va a leer el usuario final.
// Si falla, el informe muestra exactamente:
// "PDF sin título en los metadatos"
assert doc.title != "", "PDF sin título en los metadatos"
}
Regla práctica: usa require para comprobaciones obvias (la expresión ya se
explica sola) y assert cuando quien lee el informe necesita entender el
problema sin conocer el script.
Un fallo no interrumpe a los demás
check "Tres validaciones independientes" {
assert doc.page_count > 100, "pocas páginas" // falla
assert doc.title != "", "sin título" // se ejecuta igualmente
assert doc.author != "", "sin autor" // esta también
}
El informe trae todos los problemas de una vez. Es intencionado: quien recibe el archivo de vuelta quiere la lista completa de correcciones, no una cada vez.
Lo mismo ocurre entre checks: si un check da error de ejecución (por ejemplo, una variable que no existe), se convierte en un diagnóstico y los demás siguen ejecutándose.
1.3 Valores y tipos
Números y unidades
check "Números" {
x = 42 // entero
y = 2.5 // número con decimales
// Las unidades de medida se convierten a PUNTOS automáticamente
// (1 pt = 1/72 pulg):
a = 3mm // 8.5039... pt
b = 2.5cm // 70.866... pt
c = 1in // 72 pt
d = 10pt // 10 pt
// El porcentaje conserva el valor numérico:
limite = 300% // 300
require a < b // se pueden comparar directamente: todo son puntos
require c == 72.0
require limite == 300
}
Escribir 3mm en lugar de 8.504 es justamente el objetivo: el script queda
legible para quien piensa en milímetros, y la conversión no sale mal.
Textos
check "Cadenas" {
simple = "texto normal"
// Interpolación: #{...} inserta el valor de cualquier expresión
nombre = "documento.pdf"
mensaje = "Analizando #{nombre} con #{doc.page_count} páginas"
// Escapes: \n (salto de línea), \t (tabulación), \" (comillas), \\ (barra)
con_comillas = "él dijo \"hola\""
// Las barras invertidas desconocidas pasan tal cual: eso permite escribir
// expresiones regulares sin doble escape:
patron = "\d{3}\.\d{3}\.\d{3}-\d{2}" // CPF brasileño
require mensaje.contains("páginas")
}
Booleanos y qué es «verdadero»
check "Verdadero y falso" {
si = true
no = false
// Solo false y null son falsos. Todo lo demás es verdadero,
// incluidos el 0, la cadena vacía y la lista vacía.
require 0 // pasa (cero es verdadero)
require "" // pasa (la cadena vacía es verdadera)
// Por eso, para comprobar contenido, compara explícitamente:
require doc.title != "" // correcto
require doc.pages.length > 0 // correcto
}
Esto importa en las funciones que devuelven null cuando no encuentran nada:
check "Aprovechando el null" {
descripcion = data::lookup_value("lotes.csv", "L2026-08")
// null es falso, así que esto funciona directamente:
assert descripcion, "lote no encontrado en la tabla"
}
Listas
check "Listas" {
numeros = [1, 2, 3]
textos = ["a", "b", "c"]
mixta = [1, "dos", true]
require numeros.length == 3
require numeros.contains(2)
require textos.join(", ") == "a, b, c"
// El acceso es 1-based: el primer elemento es el 1
require numeros.get(1) == 1
require numeros.first() == 1
require numeros.last() == 3
}
1.4 Operadores
check "Operadores" {
// Comparación
require 10 > 5
require 10 >= 10
require 3 < 4
require 3 <= 3
require "a" == "a"
require "a" != "b"
// Aritmética
require 2 + 3 == 5
require 10 - 4 == 6
require 3 * 4 == 12
require 10 / 4 == 2.5 // la división inexacta da un número con decimales
require 10 / 5 == 2 // la exacta sigue siendo entera
// Lógica (con cortocircuito: el lado derecho solo se evalúa si hace falta)
require true && true
require false || true
require !false
// El cortocircuito en la práctica: si no hay páginas, la segunda parte
// ni siquiera se evalúa, lo que evita un error en un documento vacío.
require doc.page_count == 0 || doc.pages.first().width > 0
}
1.5 Bloques: repetir para cada elemento
Los bloques son fragmentos entre llaves que reciben un parámetro entre barras verticales. Se leen como en español: «para cada página, haz...».
check "Recorriendo páginas" {
// each: ejecuta el bloque para cada elemento
doc.pages.each { |page|
assert page.width > 0, "página #{page.number} sin anchura"
}
// each_with_index: además del elemento, recibe la posición (0, 1, 2...)
doc.fonts.each_with_index { |font, i|
print("fuente", i, ":", font.name)
}
// all: verdadero si TODOS los elementos cumplen la condición
require doc.fonts.all { |f| f.is_embedded }
// any: verdadero si ALGÚN elemento la cumple
require doc.pages.any { |p| p.extract_text() != "" }
// filter: devuelve solo los elementos que la cumplen
sin_texto = doc.pages.filter { |p| p.extract_text() == "" }
assert sin_texto.length == 0,
"#{sin_texto.length} página(s) sin texto"
// map: transforma cada elemento y devuelve una lista nueva
nombres = doc.fonts.map { |f| f.name }
print("fuentes usadas:", nombres.join(", "))
}
Los bloques se pueden encadenar, en la misma línea, sin salto antes del punto:
check "Encadenando" {
// fuentes no incrustadas, solo los nombres, unidos por comas
problemas = doc.fonts.filter { |f| !f.is_embedded }.map { |f| f.name }
assert problemas.length == 0,
"fuentes no incrustadas: #{problemas.join(", ")}"
}
Si la línea se hace demasiado larga, divídela en pasos con nombre en lugar de partir el encadenamiento: queda más legible de todos modos.
check "Pasos con nombre" {
sueltas = doc.fonts.filter { |f| !f.is_embedded }
nombres = sueltas.map { |f| f.name }
assert nombres.length == 0, "fuentes no incrustadas: #{nombres.join(", ")}"
}
1.6 Funciones: ponerle nombre a tus reglas
Cuando la misma comprobación aparece en varios sitios, dale un nombre:
// El valor de la función es el de la ÚLTIMA expresión; no existe "return".
function es_a4(page) {
abs(page.width - 595.0) < 5.0 && abs(page.height - 842.0) < 5.0
}
function excede_tac(page, limite) {
page.tac > limite
}
check "Formato y tinta" {
// ahora el check se lee casi como una frase
require doc.pages.all { |p| es_a4(p) }
doc.pages.each { |page|
assert !excede_tac(page, 300), "página #{page.number} con demasiada tinta"
}
}
Reglas de las funciones:
- Los parámetros existen solo dentro de la función.
- Pueden llamar a otras funciones.
- La recursión está permitida, pero limitada a 200 llamadas (evita bloquear el proceso).
1.7 Imports: reutilizar entre perfiles
Pon las reglas comunes en un archivo e impórtalo donde haga falta.
biblioteca.pdfl:
// Constantes y funciones compartidas por el equipo
const TAC_OFFSET = 300%
const SANGRADO_ESTANDAR = 3mm
function pagina_a4(page) {
abs(page.width - 595.0) < 5.0 && abs(page.height - 842.0) < 5.0
}
revista.pdfl:
// La ruta es relativa a ESTE archivo
import "biblioteca.pdfl"
check "Formato" {
// TAC_OFFSET y pagina_a4 vinieron del import
require doc.pages.all { |p| pagina_a4(p) }
require prepress::validate_tac_limits(TAC_OFFSET)
}
Cada archivo se carga una sola vez, aunque varios scripts lo importen, así que las importaciones circulares no bloquean nada.
1.8 Reglas (rule): validar página a página
Una rule es un check que se ejecuta una vez por cada página, con la página ya
disponible en la variable page:
// Sin "on": se ejecuta en todas las páginas
rule "Toda página tiene texto" {
assert page.extract_text().trim() != "",
"la página #{page.number} está en blanco"
}
Con on eliges en qué páginas se aplica la regla:
rule "Interior numerado" on doc.pages.filter { |p| p.number > 2 } {
pie = region(0, 0, page.width, 60)
assert text::extract_from_region(page.number, pie) != "",
"la página #{page.number} no tiene numeración en el pie"
}
Atención a la sintaxis: si la selección del
ontermina en una propiedad (por ejemplo,on doc.pages), enciérrala entre paréntesis; sin ellos, la llave{del cuerpo se interpretaría como bloque de esa llamada:rule "Ejemplo" on (doc.pages) { // con paréntesis require page.width > 0 }
1.9 Variables y ámbito
const GLOBAL = 100 // visible en todo el archivo
check "Ámbito" {
local = 42 // visible solo en este check
doc.pages.each { |page|
dentro = page.width // visible solo dentro del bloque
require dentro > 0
}
require local == 42 // sigue visible
require GLOBAL == 100 // sigue visible
}
Convención: constantes en MAYÚSCULAS, variables en minúsculas. El lenguaje no lo obliga, pero los ejemplos y los perfiles ya hechos lo siguen.
1.10 Mensajes que ayudan a quien recibe el archivo
La calidad del informe depende de los mensajes que escribas. Compara:
check "Mensajes malos" {
require doc.pages.all { |p| p.tac <= 300 }
// informe: "requirement not met: doc.pages.all() { ... }"
// — quien lo recibe no sabe qué página es ni por cuánto se pasó
}
check "Mensajes buenos" {
doc.pages.each { |page|
assert page.tac <= 300,
"Página #{page.number}: cobertura de tinta #{page.tac}% (máximo 300%)"
}
// informe: "Página 7: cobertura de tinta 324% (máximo 300%)"
// — el operador sabe exactamente qué corregir
}
Usa print() para la información de contexto que no es un error. Sale por
stderr, así que no ensucia el informe:
check "Contexto" {
print("Analizando", doc.page_count, "páginas")
print("Fuentes:", prepress::list_fonts().join(", "))
require doc.page_count > 0
}
1.11 Errores frecuentes
Los mensajes de pdfl están en inglés; la tabla asocia cada uno a su causa.
| Mensaje | Causa | Corrección |
|---|---|---|
expected end of line after statement |
dos instrucciones en la misma línea | una instrucción por línea |
unknown variable: x |
uso antes de asignar, o fuera del ámbito | declárala antes, en el mismo nivel |
unknown function: text::xyz |
nombre equivocado o función inexistente | consulta el capítulo del espacio de nombres |
fix:: is only available in the 'pdfl fix' command |
fix:: en pdfl run |
usa pdfl fix entrada.pdf script.pdfl --output salida.pdf |
unknown unit: 'kg' |
sufijo inválido | usa pt, mm, cm, in o % |
expected '{' with the rule body |
on con una selección que termina en propiedad |
encierra la selección entre paréntesis |
unexpected expression: Dot |
encadenamiento partido en varias líneas | mantén .metodo en la misma línea, o usa variables intermedias |
Antes de ejecutar, siempre vale la pena:
pdfl lint mi_perfil.pdfl # señala variables sin usar, checks duplicados...
pdfl fmt mi_perfil.pdfl # normaliza el formato