Desafios na Automação de Assinaturas de Contratos
A automação do processo de assinatura de contratos em PDF frequentemente esbarra na necessidade de posicionar dinamicamente blocos de assinatura, datas e selos digitais. Em cenários onde a interface frontend não pode fornecer as coordenadas exatas para a aplicação do carimbo e dos campos de texto, o backend deve assumir a responsabilidade de calcular essas posições.
A principal dificuldade reside em identificar a última linha de texto do documento para calcular o eixo Y adequado, garantindo que o bloco de assinatura seja inserido logo abaixo do conteúdo existente, sem sobreposições. Além disso, a formatação de tabelas para alinhar os dados das partes (Contratante e Contratado) exige um manejo preciso de layout, pois o uso de espaçamentos manuais resulta em desalinhamentos visuais.
Estratégia de Implementação
Para resolver a ausência de uma API direta de "anexar ao final" no iText, a abordagem utiliza um IEventListener para interceptar eventos de renderização de texto. Isso permite extrair as coordenadas de todos os blocos de texto da última página e determinar o ponto mais baixo (menor valor de Y).
Para o layout dos campos de assinatura, a solução mais robusta é a utilização de um componente Table sem bordas. Isso garante que os campos da parte A fiquem perfeitamente alinhados à essquerda e os da parte B à direita, dividindo a largura da página de forma proporcional. O tamanho da fonte é inferido a partir da altura das caixas delimitadoras (bounding boxes) do texto original, mantendo a consistência visual.
Implementação do Processador de PDF
A classe principal orquestra a leitura do documento, o cálculo das coordenadas, a injeção do layout de assinatura e a invocação do módulo de crpitografia.
package com.exemplo.contrato.pdf;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.geom.Vector;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfDocumentContentParser;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.borders.Border;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import java.io.File;
import java.io.IOException;
import java.util.*;
public class ContratoPdfProcessor {
public enum TipoParte {
CONTRATANTE(1),
CONTRATADO(2);
private final int id;
TipoParte(int id) { this.id = id; }
public int getId() { return id; }
}
public void processarAssinatura(String caminhoBase, String caminhoSaida, String caminhoImagemSelo,
TipoParte parte, List<String> textosAssinatura, boolean documentoJaProcessado) {
String caminhoTemp = caminhoBase.replace(".pdf", "_temp.pdf");
try {
CoordenadasDocumento limites = analisarUltimaPagina(caminhoBase);
PosicaoAssinatura coords = injetarBlocoAssinatura(limites, caminhoBase, caminhoTemp, parte.getId(), textosAssinatura, documentoJaProcessado);
byte[] pdfAssinado = AssinadorDigital.aplicarAssinaturaCriptografica(caminhoTemp, caminhoImagemSelo, coords);
GerenciadorArquivos.salvarArquivo(pdfAssinado, caminhoSaida);
} catch (Exception ex) {
throw new RuntimeException("Falha ao processar assinatura do contrato", ex);
} finally {
new File(caminhoTemp).delete();
}
}
private PosicaoAssinatura injetarBlocoAssinatura(CoordenadasDocumento limites, String entrada, String saida,
int tipoParte, List<String> textos, boolean jaProcessado) throws IOException {
try (PdfReader reader = new PdfReader(entrada);
PdfWriter writer = new PdfWriter(saida);
PdfDocument pdfDoc = new PdfDocument(reader, writer)) {
Document doc = new Document(pdfDoc);
PdfFont fontePadrao = PdfFontFactory.createFont("/System/Library/Fonts/Supplemental/Songti.ttc,1");
int totalLinhas = textos.size();
float alturaBloco = limites.getAlturaLinha() * totalLinhas;
float margemInferior = limites.getBottom() - alturaBloco;
boolean precisaNovaPagina = false;
int paginaAlvo = pdfDoc.getNumberOfPages();
if (margemInferior <= limites.getAlturaLinha() * 3) {
precisaNovaPagina = true;
if (!jaProcessado) {
pdfDoc.addNewPage();
paginaAlvo++;
}
limites.setBottom(limites.getTop() * 2 - alturaBloco * 2);
}
Table tabela = new Table(1);
tabela.setPageNumber(paginaAlvo);
float posY = (limites.getBottom() - alturaBloco) / 2;
float posX = limites.getLeft() + 30f;
if (tipoParte == TipoParte.CONTRATADO.getId()) {
posX += limites.getWidth() / 2 - 15;
}
tabela.setFixedPosition(posX, posY, 200);
tabela.setBorder(Border.NO_BORDER);
for (String texto : textos) {
Paragraph paragrafo = new Paragraph(texto).setFont(fontePadrao).setFontSize(limites.getAltura());
Cell celula = new Cell().add(paragrafo).setBorder(Border.NO_BORDER);
tabela.addCell(celula);
}
doc.add(tabela);
doc.flush();
return calcularCoordenadasFinais(limites, textos, posY, paginaAlvo, precisaNovaPagina);
}
}
private PosicaoAssinatura calcularCoordenadasFinais(CoordenadasDocumento limites, List<String> textos,
float posY, int pagina, boolean novaPagina) {
PosicaoAssinatura posicao = new PosicaoAssinatura();
int offset = novaPagina ? 2 : 3;
posicao.setY(posY + (textos.size() - offset) * limites.getAlturaLinha());
posicao.setX(limites.getLeft() + textos.get(0).length() * limites.getAltura() - 15f);
posicao.setPagina(pagina);
return posicao;
}
private CoordenadasDocumento analisarUltimaPagina(String caminhoPdf) throws IOException {
try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(caminhoPdf))) {
ExtratorCoordenadas extrator = new ExtratorCoordenadas();
new PdfDocumentContentParser(pdfDoc).processContent(pdfDoc.getNumberOfPages(), extrator);
List<Rectangle> blocos = extrator.getBlocosTexto();
float minX = Float.MAX_VALUE, maxX = 0, minY = Float.MAX_VALUE, maxY = 0;
float menorDistanciaVertical = Float.MAX_VALUE;
Rectangle anterior = null;
for (Rectangle bloco : blocos) {
if (anterior != null) {
float dist = anterior.getY() - bloco.getY();
if (dist > 5f && dist < menorDistanciaVertical) {
menorDistanciaVertical = dist;
}
}
anterior = bloco;
minX = Math.min(minX, bloco.getLeft());
maxX = Math.max(maxX, bloco.getRight());
minY = Math.min(minY, bloco.getBottom());
maxY = Math.max(maxY, bloco.getY());
}
Rectangle ultimoBloco = blocos.get(blocos.size() - 1);
CoordenadasDocumento coords = new CoordenadasDocumento();
coords.setLeft(minX);
coords.setRight(maxX);
coords.setBottom(minY);
coords.setTop(maxY);
coords.setWidth(maxX - minX);
coords.setHeight(ultimoBloco.getHeight());
coords.setAlturaLinha(menorDistanciaVertical);
return coords;
}
}
private static class ExtratorCoordenadas implements IEventListener {
private final List<Rectangle> blocosTexto = new ArrayList<>();
@Override
public void eventOccurred(IEventData data, EventType type) {
if (type == EventType.RENDER_TEXT) {
TextRenderInfo info = (TextRenderInfo) data;
if (info.getText().trim().isEmpty()) return;
Vector inicio = info.getDescentLine().getStartPoint();
Vector fim = info.getAscentLine().getEndPoint();
float x1 = Math.min(inicio.get(0), fim.get(0));
float y1 = Math.min(inicio.get(1), fim.get(1));
float x2 = Math.max(inicio.get(0), fim.get(0));
float y2 = Math.max(inicio.get(1), fim.get(1));
blocosTexto.add(new Rectangle(x1, y1, x2 - x1, y2 - y1));
}
}
@Override
public Set<EventType> getSupportedEvents() {
return Collections.singleton(EventType.RENDER_TEXT);
}
public List<Rectangle> getBlocosTexto() { return blocosTexto; }
}
}
Estruturas de Dados Auxiliares
As classes de transferência de dados (DTOs) encapsulam as métricas espaciais extraídas do documento e as coordenadas finais para a aplicação do selo.
package com.exemplo.contrato.pdf;
public class CoordenadasDocumento {
private float width, left, right, bottom, top, height, alturaLinha;
// Getters e Setters omitidos para brevidade
public float getWidth() { return width; }
public void setWidth(float width) { this.width = width; }
public float getLeft() { return left; }
public void setLeft(float left) { this.left = left; }
public float getRight() { return right; }
public void setRight(float right) { this.right = right; }
public float getBottom() { return bottom; }
public void setBottom(float bottom) { this.bottom = bottom; }
public float getTop() { return top; }
public void setTop(float top) { this.top = top; }
public float getHeight() { return height; }
public void setHeight(float height) { this.height = height; }
public float getAlturaLinha() { return alturaLinha; }
public void setAlturaLinha(float alturaLinha) { this.alturaLinha = alturaLinha; }
}
package com.exemplo.contrato.pdf;
public class PosicaoAssinatura {
private float x, y;
private int pagina;
public float getX() { return x; }
public void setX(float x) { this.x = x; }
public float getY() { return y; }
public void setY(float y) { this.y = y; }
public int getPagina() { return pagina; }
public void setPagina(int pagina) { this.pagina = pagina; }
}
Módulo de Assinatura Criptográfica
A aplicação do selo visual e da assinatura digital invisível é realizada utilizando BouncyCastle para manipulação do keystore PKCS12 e das APIs de segurança do iText.
package com.exemplo.contrato.pdf;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.security.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.util.UUID;
public class AssinadorDigital {
public static byte[] aplicarAssinaturaCriptografica(String caminhoPdf, String caminhoImagem, PosicaoAssinatura coords) {
Security.addProvider(new BouncyCastleProvider());
try (InputStream ksStream = AssinadorDigital.class.getClassLoader().getResourceAsStream("certificado.p12");
PdfReader reader = new PdfReader(caminhoPdf);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
KeyStore ks = KeyStore.getInstance("PKCS12", "BC");
ks.load(ksStream, "senha_segura".toCharArray());
String alias = ks.aliases().nextElement();
PrivateKey chavePrivada = (PrivateKey) ks.getKey(alias, "senha_segura".toCharArray());
Certificate[] cadeia = ks.getCertificateChain(alias);
PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0', null, true);
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
appearance.setReason("Assinatura de Contrato Digital");
appearance.setSignatureGraphic(Image.getInstance(caminhoImagem));
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
float tamanhoSelo = 120f;
Rectangle areaSelo = new Rectangle(coords.getX(), coords.getY(), coords.getX() + tamanhoSelo, coords.getY() + tamanhoSelo);
appearance.setVisibleSignature(areaSelo, coords.getPagina(), UUID.randomUUID().toString());
ExternalDigest digest = new BouncyCastleDigest();
ExternalSignature signature = new PrivateKeySignature(chavePrivada, DigestAlgorithms.SHA256, "BC");
MakeSignature.signDetached(appearance, digest, signature, cadeia, null, null, null, 0, MakeSignature.CryptoStandard.CADES);
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException("Erro durante a assinatura criptográfica", e);
}
}
}
Utilitário de Persistência
Método auxiliar para gravar o array de bytes resultante no sistema de arquivos local, garantindo a criação dos diretórios pais caso não existam.
package com.exemplo.contrato.pdf;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class GerenciadorArquivos {
public static void salvarArquivo(byte[] dados, String destino) throws Exception {
Path caminhoDestino = Paths.get(destino);
Path diretorioPai = caminhoDestino.getParent();
if (diretorioPai != null && !Files.exists(diretorioPai)) {
Files.createDirectories(diretorioPai);
}
try (FileOutputStream fos = new FileOutputStream(destino)) {
fos.write(dados);
fos.flush();
}
}
}