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 on termina 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

← Índice · Siguiente: Tipos del documento →