1. A linguagem PDFLang
← Índice · Próximo: Tipos do documento →
PDFLang foi desenhada para ser lida por quem não programa. Não há classes, herança, tipos declarados nem ponto-e-vírgula. Um script é uma lista de verificações escritas quase em português.
1.1 A estrutura de um script
// Comentários começam com duas barras e vão até o fim da linha.
profile "nome-do-perfil" { // profile é opcional: agrupa e nomeia o
// conjunto; o nome aparece no relatório.
const LIMITE = 300% // constantes: convenção de MAIÚSCULAS
check "Nome do Check" { // cada check vira uma seção do relatório
require doc.page_count > 0 // uma validação
}
check "Outro Check" { // quantos checks você quiser
require doc.title != ""
}
}
O profile é opcional — um script pode ter apenas checks soltos:
check "Simples" {
require doc.page_count > 0
}
Tags nos checks
Tags servem para organizar e filtrar visualmente os checks no relatório:
check "TAC dentro do limite" tags: ["prepress", "cores"] {
require prepress::validate_tac_limits(300)
}
1.2 As duas formas de validar
Toda validação usa require ou assert. A diferença é só a mensagem que
aparece no relatório quando a validação falha.
check "Comparando as duas formas" {
// require: a mensagem é gerada da própria expressão.
// Se falhar, o relatório mostra:
// "requirement not met: doc.page_count > 0"
require doc.page_count > 0
// assert: você escreve a mensagem que o usuário final vai ler.
// Se falhar, o relatório mostra exatamente:
// "PDF sem título nos metadados"
assert doc.title != "", "PDF sem título nos metadados"
}
Regra prática: use require para verificações óbvias (a expressão já se
explica) e assert quando quem lê o relatório precisa entender o problema sem
conhecer o script.
Uma falha não interrompe as outras
check "Três validações independentes" {
assert doc.page_count > 100, "poucas páginas" // falha
assert doc.title != "", "sem título" // roda mesmo assim
assert doc.author != "", "sem autor" // esta também
}
O relatório traz todos os problemas de uma vez. Isso é proposital: quem recebe o arquivo de volta quer a lista completa de correções, não uma por vez.
O mesmo vale entre checks — se um check der erro de execução (por exemplo, uma variável que não existe), ele vira um diagnóstico e os demais continuam rodando.
1.3 Valores e tipos
Números e unidades
check "Números" {
x = 42 // inteiro
y = 2.5 // número com decimais
// Unidades de medida viram PONTOS automaticamente (1 pt = 1/72 pol):
a = 3mm // 8.5039... pt
b = 2.5cm // 70.866... pt
c = 1in // 72 pt
d = 10pt // 10 pt
// Porcentagem mantém o valor numérico:
limite = 300% // 300
require a < b // dá para comparar direto, tudo é ponto
require c == 72.0
require limite == 300
}
Escrever 3mm em vez de 8.504 é o ponto: o script fica legível para quem
pensa em milímetros, e a conversão não sai errada.
Textos
check "Strings" {
simples = "texto comum"
// Interpolação: #{...} insere o valor de qualquer expressão
nome = "documento.pdf"
mensagem = "Analisando #{nome} com #{doc.page_count} páginas"
// Escapes: \n (nova linha), \t (tabulação), \" (aspas), \\ (barra)
com_aspas = "ele disse \"olá\""
// Barras invertidas desconhecidas passam direto — isso permite escrever
// expressões regulares sem escape duplo:
padrao = "\d{3}\.\d{3}\.\d{3}-\d{2}" // CPF
require mensagem.contains("páginas")
}
Booleanos e o que é "verdadeiro"
check "Verdadeiro e falso" {
sim = true
nao = false
// Só false e null são falsos. Todo o resto é verdadeiro —
// inclusive 0, string vazia e lista vazia.
require 0 // passa (zero é verdadeiro)
require "" // passa (string vazia é verdadeira)
// Por isso, para testar conteúdo, compare explicitamente:
require doc.title != "" // certo
require doc.pages.length > 0 // certo
}
Isso importa em funções que devolvem null quando não encontram nada:
check "Aproveitando o null" {
descricao = data::lookup_value("lotes.csv", "L2026-08")
// null é falso, então isto funciona diretamente:
assert descricao, "lote não encontrado na tabela"
}
Listas
check "Listas" {
numeros = [1, 2, 3]
textos = ["a", "b", "c"]
misto = [1, "dois", true]
require numeros.length == 3
require numeros.contains(2)
require textos.join(", ") == "a, b, c"
// Acesso é 1-based: o primeiro item é o item 1
require numeros.get(1) == 1
require numeros.first() == 1
require numeros.last() == 3
}
1.4 Operadores
check "Operadores" {
// Comparação
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 // divisão inexata vira número com decimais
require 10 / 5 == 2 // exata continua inteiro
// Lógica (com curto-circuito: o lado direito só é avaliado se necessário)
require true && true
require false || true
require !false
// Curto-circuito na prática: se não há páginas, a segunda parte
// nem é avaliada — evita erro em documento vazio.
require doc.page_count == 0 || doc.pages.first().width > 0
}
1.5 Blocos: repetindo para cada item
Blocos são trechos entre chaves que recebem um parâmetro entre barras verticais. É como se lê em português: "para cada página, faça...".
check "Percorrendo páginas" {
// each: executa o bloco para cada item
doc.pages.each { |page|
assert page.width > 0, "página #{page.number} sem largura"
}
// each_with_index: além do item, recebe a posição (0, 1, 2...)
doc.fonts.each_with_index { |font, i|
print("fonte", i, ":", font.name)
}
// all: verdadeiro se TODOS os itens satisfazem a condição
require doc.fonts.all { |f| f.is_embedded }
// any: verdadeiro se ALGUM item satisfaz
require doc.pages.any { |p| p.extract_text() != "" }
// filter: devolve só os itens que satisfazem
sem_texto = doc.pages.filter { |p| p.extract_text() == "" }
assert sem_texto.length == 0,
"#{sem_texto.length} página(s) sem texto"
// map: transforma cada item, devolvendo uma nova lista
nomes = doc.fonts.map { |f| f.name }
print("fontes usadas:", nomes.join(", "))
}
Blocos podem ser encadeados — na mesma linha, sem quebra antes do ponto:
check "Encadeando" {
// fontes não embutidas, só os nomes, unidos por vírgula
problemas = doc.fonts.filter { |f| !f.is_embedded }.map { |f| f.name }
assert problemas.length == 0,
"fontes não embutidas: #{problemas.join(", ")}"
}
Se a linha ficar longa demais, quebre em etapas nomeadas em vez de quebrar o encadeamento — fica mais legível de qualquer forma:
check "Etapas nomeadas" {
soltas = doc.fonts.filter { |f| !f.is_embedded }
nomes = soltas.map { |f| f.name }
assert nomes.length == 0, "fontes não embutidas: #{nomes.join(", ")}"
}
1.6 Functions: dando nome às suas regras
Quando a mesma verificação aparece em vários lugares, dê um nome a ela:
// O valor da function é o da ÚLTIMA expressão — não existe "return".
function eh_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 e tinta" {
// agora o check se lê quase como uma frase
require doc.pages.all { |p| eh_a4(p) }
doc.pages.each { |page|
assert !excede_tac(page, 300), "página #{page.number} com tinta demais"
}
}
Regras das functions:
- Os parâmetros existem só dentro da function.
- Podem chamar outras functions.
- Recursão é permitida, mas limitada a 200 chamadas (evita travar o processo).
1.7 Imports: reaproveitando entre perfis
Coloque as regras comuns em um arquivo e importe onde precisar.
biblioteca.pdfl:
// Constantes e functions compartilhadas pela equipe
const TAC_OFFSET = 300%
const SANGRIA_PADRAO = 3mm
function pagina_a4(page) {
abs(page.width - 595.0) < 5.0 && abs(page.height - 842.0) < 5.0
}
revista.pdfl:
// O caminho é relativo a ESTE arquivo
import "biblioteca.pdfl"
check "Formato" {
// TAC_OFFSET e pagina_a4 vieram do import
require doc.pages.all { |p| pagina_a4(p) }
require prepress::validate_tac_limits(TAC_OFFSET)
}
Cada arquivo é carregado uma única vez, mesmo que vários scripts o importem — então importações circulares não travam.
1.8 Regras (rule): validar página a página
Uma rule é um check que roda uma vez para cada página, com a página já
disponível na variável page:
// Sem "on": roda em todas as páginas
rule "Toda página tem texto" {
assert page.extract_text().trim() != "",
"página #{page.number} está em branco"
}
Com on, você escolhe em quais páginas a regra se aplica:
rule "Miolo numerado" on doc.pages.filter { |p| p.number > 2 } {
rodape = region(0, 0, page.width, 60)
assert text::extract_from_region(page.number, rodape) != "",
"página #{page.number} sem numeração no rodapé"
}
Atenção à sintaxe: se a seleção do
onterminar em uma propriedade (ex.:on doc.pages), envolva-a em parênteses — sem elas, a chave{do corpo seria interpretada como bloco daquela chamada:rule "Exemplo" on (doc.pages) { // com parênteses require page.width > 0 }
1.9 Variáveis e escopo
const GLOBAL = 100 // visível no arquivo inteiro
check "Escopo" {
local = 42 // visível só neste check
doc.pages.each { |page|
dentro = page.width // visível só dentro do bloco
require dentro > 0
}
require local == 42 // ainda visível
require GLOBAL == 100 // ainda visível
}
Convenção: constantes em MAIÚSCULAS, variáveis em minúsculas. A linguagem não obriga, mas os exemplos e perfis prontos seguem isso.
1.10 Mensagens que ajudam quem recebe o arquivo
A qualidade do relatório depende das mensagens que você escreve. Compare:
check "Mensagens ruins" {
require doc.pages.all { |p| p.tac <= 300 }
// relatório: "requirement not met: doc.pages.all() { ... }"
// — quem recebe não sabe qual página nem quanto excedeu
}
check "Mensagens boas" {
doc.pages.each { |page|
assert page.tac <= 300,
"Página #{page.number}: cobertura de tinta #{page.tac}% (máximo 300%)"
}
// relatório: "Página 7: cobertura de tinta 324% (máximo 300%)"
// — o operador sabe exatamente o que corrigir
}
Use print() para informação de contexto que não é erro. Ela sai no stderr,
então não polui o relatório:
check "Contexto" {
print("Analisando", doc.page_count, "páginas")
print("Fontes:", prepress::list_fonts().join(", "))
require doc.page_count > 0
}
1.11 Erros comuns
As mensagens do pdfl são em inglês; a tabela liga cada uma à causa.
| Mensagem | Causa | Correção |
|---|---|---|
expected end of line after statement |
dois comandos na mesma linha | um comando por linha |
unknown variable: x |
uso antes de atribuir, ou fora do escopo | declare antes, no mesmo nível |
unknown function: text::xyz |
nome errado ou função inexistente | veja o capítulo do namespace |
fix:: is only available in the 'pdfl fix' command |
fix:: em pdfl run |
use pdfl fix entrada.pdf script.pdfl --output saida.pdf |
unknown unit: 'kg' |
sufixo inválido | use pt, mm, cm, in ou % |
expected '{' with the rule body |
on com seleção terminando em propriedade |
envolva a seleção em parênteses |
unexpected expression: Dot |
encadeamento quebrado em várias linhas | mantenha .metodo na mesma linha, ou use variáveis intermediárias |
Antes de rodar, vale sempre:
pdfl lint meu_perfil.pdfl # aponta variáveis não usadas, checks duplicados...
pdfl fmt meu_perfil.pdfl # padroniza a formatação