Mesclar e extrair páginas de PDF com PDFtk e Python
A operação cat do PDFtk pode juntar PDFs, extrair um intervalo de páginas ou reordenar
páginas sem rasterizá-las. Este passo a passo usa o PDFtk Java para mesclar dois pequenos documentos
de exemplo, selecionar páginas do resultado e automatizar a mesclagem em Python, com status de saída
diferente de zero em caso de falha e sem substituir arquivos de saída existentes.
Requisitos do sistema
Os comandos abaixo foram testados no Linux com Bash 5.3.15, OpenJDK 21.0.12.1, Python 3.14.7,
qpdf 12.4.1 e Poppler 26.08.0. Tenha java, python3, qpdf, pdftotext, curl e
sha256sum no seu PATH antes de começar. Essas são as versões testadas, e não uma afirmação de que
toda versão mais antiga funciona. O Poppler fornece pdftotext; o qpdf verifica a estrutura do PDF
independentemente do PDFtk.
Use PDFs confiáveis e sem criptografia neste exemplo local. O fato de um parser aceitar um PDF não estabelece que ele seja inofensivo, visualmente íntegro ou livre de conteúdo ativo. Execute documentos não confiáveis em um ambiente devidamente isolado, e não por meio deste script como filtro de segurança.
Instalação do PDFtk
O PDFtk Java é um port do PDFtk, e não o executável nativo original do PDFtk Server. O
guia de instalação versionado dele
lista pacotes de distribuições e um JAR independente que contém suas dependências. Usamos esse JAR
para que toda invocação selecione a versão 3.3.3, em vez de qualquer pdftk que um gerenciador de
pacotes forneça. Este passo a passo usa comandos de shell do Linux; ele não aborda instaladores para
macOS ou Windows.
Cole este bloco no Bash a partir de um diretório com permissão de escrita. Ele cria um novo diretório
pdf-workflow e mantém seu shell no diretório original. Um diretório existente, um download com falha
ou uma divergência de checksum interrompe o bloco. Não exclua um projeto existente para fazê-lo
funcionar.
(
set -eu
mkdir pdf-workflow
cd pdf-workflow
curl -fsSLo pdftk-java-3.3.3-all.jar \
https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v3.3.3/pdftk-all.jar
printf '%s %s\n' \
a694d49bd03e1edd4c23b3ba808bc221eb8a8ccfe7bfd2a0a884b2b2fb425188 \
pdftk-java-3.3.3-all.jar | sha256sum -c -
java -Xms16m -Xmx256m -XX:ActiveProcessorCount=2 -XX:-UsePerfData \
-Djava.io.tmpdir="$PWD" -Duser.home="$PWD" \
-jar pdftk-java-3.3.3-all.jar --version
)
O hash fixa os bytes baixados; ele não é uma assinatura do publicador. A saída de versão deve identificar o PDFtk Java 3.3.3. Uma configuração com falha pode deixar para trás o novo diretório e o arquivo baixado; inspecione-os antes de escolher um novo local.
Operações básicas com PDF
Para ter uma entrada reproduzível, salve o conteúdo a seguir como pdf-workflow/make_samples.py. Ele usa apenas a
biblioteca padrão do Python para criar um PDF de duas páginas com os rótulos ALPHA ONE e ALPHA TWO, e um
PDF de uma página com o rótulo BETA ONE. Ele recusa nomes de arquivo de exemplo já existentes em vez
de substituí-los.
from pathlib import Path
def write_pdf(path, labels):
page_ids = [4 + 2 * index for index in range(len(labels))]
kids = ' '.join(f'{page_id} 0 R' for page_id in page_ids)
objects = [
b'<< /Type /Catalog /Pages 2 0 R >>',
f'<< /Type /Pages /Count {len(labels)} /Kids [{kids}] >>'.encode(),
b'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
]
for page_id, label in zip(page_ids, labels):
stream = f'BT /F1 24 Tf 40 180 Td ({label}) Tj ET\n'.encode('ascii')
objects.append(
f'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 360 240] '
f'/Resources << /Font << /F1 3 0 R >> >> '
f'/Contents {page_id + 1} 0 R >>'.encode()
)
objects.append(f'<< /Length {len(stream)} >>\nstream\n'.encode()
+ stream + b'endstream')
data = bytearray(b'%PDF-1.4\n')
offsets = [0]
for object_id, payload in enumerate(objects, start=1):
offsets.append(len(data))
data.extend(f'{object_id} 0 obj\n'.encode() + payload + b'\nendobj\n')
xref = len(data)
data.extend(f'xref\n0 {len(offsets)}\n0000000000 65535 f \n'.encode())
for offset in offsets[1:]:
data.extend(f'{offset:010d} 00000 n \n'.encode())
data.extend(f'trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\n'
f'startxref\n{xref}\n%%EOF\n'.encode())
with path.open('xb') as output:
output.write(data)
write_pdf(Path('input-a.pdf'), ['ALPHA ONE', 'ALPHA TWO'])
write_pdf(Path('input-b.pdf'), ['BETA ONE'])
Mesclagem de PDFs
Execute isto a partir do mesmo diretório pai usado na configuração. A e B são handles de
entrada; A1-end B1-end inclui todas as páginas de A seguidas de todas as páginas de B. O
manual do PDFtk documenta intervalos de páginas e
handles. Com seus próprios documentos, substitua os nomes de entrada e confira antes a contagem de
páginas deles.
(
set -eu
cd pdf-workflow
python3 make_samples.py
java -Xms16m -Xmx256m -XX:ActiveProcessorCount=2 -XX:-UsePerfData \
-Djava.io.tmpdir="$PWD" -Duser.home="$PWD" \
-jar pdftk-java-3.3.3-all.jar A=input-a.pdf B=input-b.pdf \
cat A1-end B1-end output combined.pdf dont_ask
)
Os comandos de baixo nível usam dont_ask deliberadamente: eles são executados sem prompts e
sobrescrevem suas saídas nomeadas. São comandos de exemplo sequenciais, e não wrappers seguros de
publicação, e uma falha pode deixar uma saída parcial. Não os aponte para arquivos que você precisa
preservar. O script de mesclagem em Python abaixo usa uma política diferente, sem substituição.
Executar novamente o bloco de mesclagem inteiro faz com que ele seja interrompido nos arquivos de
exemplo existentes; ele não continua usando entradas desatualizadas depois dessa falha.
Divisão de PDFs
Extraia e reordene páginas usando o PDF mesclado gerado acima. As páginas são numeradas a partir de
um. cat 3 1 seleciona a terceira página dele seguida da primeira; cat 1-2 selecionaria um intervalo
contíguo.
(
set -eu
cd pdf-workflow
java -Xms16m -Xmx256m -XX:ActiveProcessorCount=2 -XX:-UsePerfData \
-Djava.io.tmpdir="$PWD" -Duser.home="$PWD" \
-jar pdftk-java-3.3.3-all.jar combined.pdf cat 3 1 output selected.pdf dont_ask
)
Verifique as duas saídas reais, e não apenas o status de saída do processo que as gerou:
(
set -eu
cd pdf-workflow
qpdf --check combined.pdf >/dev/null
test "$(qpdf --show-npages combined.pdf)" = 3
printf 'Merged pages:\n'
pdftotext -layout combined.pdf -
qpdf --check selected.pdf >/dev/null
test "$(qpdf --show-npages selected.pdf)" = 2
printf 'Selected pages:\n'
pdftotext -layout selected.pdf -
)
O texto mesclado deve ser ALPHA ONE, ALPHA TWO e depois BETA ONE; o texto selecionado deve ser
BETA ONE e depois ALPHA ONE. Abra os dois PDFs em um visualizador para conferir a aparência. Essas
verificações de texto e estrutura não provam que documentos arbitrários preservem formulários,
anotações, assinaturas ou todos os detalhes visuais. Digitalizações compostas apenas por imagens
podem não ter texto extraível.
Otimização do tamanho de arquivos PDF
A opção compress do PDFtk restaura a
compressão dos fluxos de conteúdo das páginas; ela não é um redutor de resolução de imagens nem uma
garantia de um PDF menor. A mesclagem também não “padroniza” um documento. A compressão de imagens
exige um fluxo de trabalho de reescrita separado, com suas próprias verificações de qualidade, por
isso não há uma etapa de conversão com Ghostscript neste exemplo.
Exemplo de integração
Salve isto como pdf-workflow/merge_pdfs.py. Ele aceita um JAR, um novo nome de arquivo de saída e uma ou mais
entradas. Coloque -- antes do argumento do JAR, como na invocação abaixo, para que o analisador
de argumentos do Python trate literalmente os hifens iniciais em nomes de arquivo. O script resolve
os caminhos de entrada antes de passá-los como argumentos separados do subprocesso; ele não monta um
comando de shell a partir dos seus nomes de arquivo.
Este script local para Linux rejeita entradas cujo tamanho combinado ultrapasse 100 MiB, verifica
cada entrada com o qpdf e grava o PDF mesclado em uma área de preparação no diretório de destino.
Depois de verificar o PDF preparado, ele usa
os.link para publicar sem substituir um
destino existente. O sistema de arquivos precisa oferecer suporte a hard links. Ele não promete
durabilidade em caso de falha do sistema nem operação segura caso alguém altere maliciosamente o
diretório ou as entradas durante o processamento.
import argparse
import os
import subprocess
import sys
import tempfile
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description='Merge PDFs without replacing an output.')
parser.add_argument('jar', type=Path)
parser.add_argument('output', type=Path)
parser.add_argument('inputs', type=Path, nargs='+')
args = parser.parse_args()
destination = args.output.absolute()
if os.path.lexists(destination):
raise FileExistsError(f'Output already exists: {destination}')
jar = args.jar.resolve(strict=True)
inputs = [path.resolve(strict=True) for path in args.inputs]
if not jar.is_file() or any(not path.is_file() for path in inputs):
raise ValueError('The JAR and inputs must be regular files.')
if sum(path.stat().st_size for path in inputs) > 100 * 1024 * 1024:
raise ValueError('Combined inputs exceed 100 MiB.')
for path in inputs:
subprocess.run(['qpdf', '--check', str(path)], check=True,
capture_output=True, timeout=60)
with tempfile.TemporaryDirectory(prefix='.pdf-merge-', dir=destination.parent) as staging:
staged = Path(staging) / 'merged.pdf'
command = [
'java', '-Xms16m', '-Xmx256m', '-XX:ActiveProcessorCount=2', '-XX:-UsePerfData',
f'-Djava.io.tmpdir={staging}', f'-Duser.home={staging}', '-jar', str(jar),
*map(str, inputs), 'cat', 'output', str(staged), 'dont_ask',
]
subprocess.run(command, check=True, capture_output=True, timeout=60)
subprocess.run(['qpdf', '--check', str(staged)], check=True,
capture_output=True, timeout=60)
os.link(staged, destination)
print(f'Created {destination.name}')
if __name__ == '__main__':
try:
main()
except (OSError, ValueError, subprocess.SubprocessError) as error:
print(f'Cannot merge PDFs: {error}', file=sys.stderr)
sys.exit(1)
Execute-o com as mesmas entradas de exemplo. A saída tem três páginas na mesma ordem que
combined.pdf:
(
set -eu
cd pdf-workflow
python3 merge_pdfs.py -- pdftk-java-3.3.3-all.jar merged-from-python.pdf input-a.pdf input-b.pdf
qpdf --check merged-from-python.pdf >/dev/null
test "$(qpdf --show-npages merged-from-python.pdf)" = 3
pdftotext -layout merged-from-python.pdf -
)
Arquivos ausentes, PDFs inválidos, entradas grandes demais, um executável ausente, um tempo limite
esgotado ou uma publicação com falha retornam um status diferente de zero sem relatar uma mesclagem
bem-sucedida. Uma segunda invocação recusa merged-from-python.pdf e mantém seus bytes inalterados. Falhas na
verificação prévia não criam saída preparada; falhas comuns durante o processamento limpam o
diretório de preparação. Um encerramento forçado pode deixar esse diretório privado para trás. Cada
subprocesso nativo tem um prazo de 60 segundos, e não um prazo de 60 segundos para a mesclagem
inteira. Avisos do qpdf são tratados como falhas aqui; inspecione o arquivo de origem em vez de
aceitar silenciosamente um documento reparado.
Solução de problemas comuns
Erros relacionados à memória
A verificação de 100 MiB é uma política de admissão de entradas, e não uma estimativa de RAM. A
complexidade do PDF, a descompressão e as alocações fora do heap do Java também importam. -Xmx256m
limita o heap do Java, e não a memória total do processo, e não garante que uma entrada aceita será
concluída. Não há divisão automática em lotes: um documento grande demais é rejeitado, e não
colocado em um lote grande demais nem precedido por um lote vazio.
Erros de acesso a arquivos
Confira se as entradas e o JAR podem ser lidos e se o diretório pai da saída existe e permite escrita. Escolha um novo destino quando já existir um; o script em Python não o exclui, mesmo que outra etapa falhe. Não afrouxe amplamente as permissões de arquivo só para fazer a mesclagem funcionar. Em caso de falha do qpdf ou do PDFtk, execute o comando correspondente localmente na entrada problemática para inspecionar o diagnóstico.
Para começar, processe uma tarefa por vez. Arquivos de entrada menores, por si só, não garantem memória limitada nem maior vazão; adicione concorrência somente depois de medir sua própria carga de trabalho e os recursos disponíveis.
Considerações de segurança
Manuseio seguro de PDFs
O modo encrypt_128bit do PDFtk
usa o RC4 legado, e não o AES moderno. Não o use para proteger documentos confidenciais. Uma senha
de proprietário, sozinha, não exige senha para abrir um PDF; a senha de usuário é o mecanismo
separado de senha na abertura. Consulte as
opções de senha ao
manter um fluxo de trabalho legado, em vez de copiar senhas para argumentos de comando ou scripts.
PROMPT está disponível para digitar senhas interativamente; este script de mesclagem não interativo
não lida com entradas protegidas por senha.
Gerenciamento de permissões de arquivo
As restrições de impressão e modificação de PDFs dependem de o leitor de PDF respeitá-las. Elas não são uma barreira de controle de acesso. Restrinja o acesso aos próprios arquivos; o script de mesclagem não remove texto ou metadados confidenciais, nem define a política de armazenamento e compartilhamento de uma organização.
Conclusão
Use intervalos de páginas explícitos quando a ordem importar, inspecione o documento resultante e decida se substituir uma saída é aceitável antes de automatizar o comando. O projeto PDFtk Java é o lugar para explorar operações adicionais quando este pequeno fluxo de trabalho atender às suas necessidades.
