Combina y extrae páginas PDF con PDFtk y Python
La operación cat de PDFtk permite unir PDF, extraer un rango de páginas
o reordenarlas sin rasterizarlas. Este tutorial usa PDFtk Java para combinar dos documentos de
muestra pequeños y seleccionar páginas del resultado. También automatiza la combinación en Python:
el script de Python devuelve un estado de salida distinto de cero si falla y no reemplaza archivos
de salida existentes.
Requisitos del sistema
Los comandos siguientes se probaron en Linux con Bash 5.3.15, OpenJDK 21.0.12.1, Python 3.14.7,
qpdf 12.4.1 y Poppler 26.08.0. Antes de empezar, asegúrate de tener java,
python3, qpdf, pdftotext, curl y
sha256sum en tu PATH. Estas son las versiones probadas;
no se afirma que todas las versiones anteriores funcionen. Poppler proporciona
pdftotext; qpdf comprueba la estructura del PDF independientemente de PDFtk.
Usa PDF de confianza y sin cifrar para este ejemplo local. Que un analizador acepte un PDF no demuestra que sea inofensivo, que conserve su aspecto visual ni que esté libre de contenido activo. Procesa los documentos que no sean de confianza en un entorno debidamente aislado, no mediante este script como filtro de seguridad.
Instalar PDFtk
PDFtk Java es una adaptación de PDFtk, no el ejecutable nativo original PDFtk Server. Su
guía de instalación para esta versión
enumera los paquetes de las distribuciones y un JAR independiente que contiene sus dependencias.
Usamos ese JAR para que cada invocación seleccione la versión 3.3.3, en lugar del
pdftk que proporcione un gestor de paquetes. Este tutorial usa comandos de
shell de Linux; no cubre los instaladores de macOS ni de Windows.
Pega este bloque en Bash desde un directorio con permiso de escritura. Crea un directorio nuevo
pdf-workflow y deja tu shell en su directorio original. Si el directorio ya existe,
la descarga falla o la suma de comprobación no coincide, el bloque se detiene. No elimines un
proyecto existente para que funcione.
(
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
)
El hash fija los bytes de la descarga; no es una firma del editor. La salida de versión debe identificar PDFtk Java 3.3.3. Si la preparación falla, pueden quedar el directorio nuevo y el archivo descargado; inspecciónalos antes de elegir una ubicación nueva.
Operaciones básicas con PDF
Para obtener una entrada reproducible, guarda lo siguiente como pdf-workflow/make_samples.py.
Usa únicamente la biblioteca estándar de Python para crear un PDF de dos páginas con las etiquetas
ALPHA ONE y ALPHA TWO, y un PDF de una página con la etiqueta
BETA ONE. Rechaza los nombres de archivo de muestra existentes en lugar de
reemplazar esos archivos.
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'])
Combinar PDF
Ejecuta esto desde el mismo directorio padre que usaste para la preparación.
A y B son identificadores de entrada;
A1-end B1-end incluye todas las páginas de A seguidas de todas las páginas de B. El
manual de PDFtk documenta los rangos de páginas y los identificadores.
Con tus propios documentos, reemplaza los nombres de entrada y comprueba primero sus cantidades de
páginas.
(
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
)
Los comandos de bajo nivel usan deliberadamente dont_ask: se ejecutan sin pedir
confirmación y sobrescriben los archivos de salida indicados. Son comandos de ejemplo
secuenciales, no mecanismos de publicación segura, y un fallo puede dejar una salida parcial. No
los dirijas a archivos que necesites conservar. El script de combinación en Python que aparece más
adelante usa una política diferente que impide reemplazar archivos. Si vuelves a ejecutar el bloque
completo de combinación, se detiene al encontrar los archivos de muestra existentes; no continúa
usando entradas antiguas después de ese fallo.
Dividir PDF
Extrae y reordena páginas usando el PDF combinado que se produjo antes. Las páginas se numeran a
partir de uno. cat 3 1 selecciona su tercera página seguida de la primera;
cat 1-2 seleccionaría un rango contiguo.
(
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
)
Comprueba ambos archivos de salida reales, no solo el estado de salida del programa que los genera:
(
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 -
)
El texto combinado debe ser ALPHA ONE, ALPHA TWO y luego
BETA ONE; el texto seleccionado debe ser BETA ONE y luego
ALPHA ONE. Abre ambos PDF en un visor para comprobar su apariencia. Estas
comprobaciones de texto y estructura no demuestran que cualquier documento conserve formularios,
anotaciones, firmas ni todos los detalles visuales. Los documentos escaneados que solo contienen
imágenes pueden no tener texto extraíble.
Optimizar el tamaño de los archivos PDF
La opción compress de PDFtk restablece la
compresión de los flujos de página; no reduce la resolución de las imágenes ni garantiza un PDF más
pequeño. La combinación tampoco «estandariza» un documento. La compresión de imágenes requiere un
flujo de trabajo de reescritura independiente con sus propias comprobaciones de calidad, por lo
que este ejemplo no incluye un paso de conversión con Ghostscript.
Ejemplo de integración
Guarda esto como pdf-workflow/merge_pdfs.py. Acepta un JAR, un nombre nuevo de archivo de salida
y una o más entradas. Coloca -- antes del argumento del JAR, como en la
invocación siguiente, para que el analizador de argumentos de Python trate literalmente los guiones
iniciales de los nombres de archivo. El script resuelve las rutas de entrada antes de pasarlas como
argumentos separados al subproceso; no construye un comando de shell a partir de tus nombres de
archivo.
Este script local para Linux rechaza las entradas cuyo tamaño combinado supere los 100 MiB,
comprueba cada entrada con qpdf y prepara temporalmente el PDF combinado en el directorio de destino.
Después de comprobar el PDF temporal, usa
os.link para publicarlo sin reemplazar un
destino existente. El sistema de archivos debe admitir enlaces duros. El script no garantiza la
durabilidad ante una caída ni un funcionamiento seguro frente a alguien que modifique de forma
maliciosa el directorio o las entradas durante el procesamiento.
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)
Ejecútalo con las mismas entradas de muestra. La salida tiene tres páginas en el mismo orden 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 -
)
Los archivos ausentes, los PDF no válidos, las entradas demasiado grandes, la ausencia de un
ejecutable, un tiempo de espera agotado o un fallo de publicación devuelven un estado distinto de
cero sin informar que la combinación fue exitosa. Una segunda invocación rechaza
merged-from-python.pdf y deja sus bytes intactos. Los fallos en las comprobaciones previas
no crean ninguna salida temporal; los fallos ordinarios durante el procesamiento eliminan el
directorio temporal. Una terminación forzada puede dejar ese directorio privado. Cada subproceso
nativo tiene un plazo de 60 segundos; no es un plazo de 60 segundos para toda la combinación. Aquí
las advertencias de qpdf se tratan como fallos; inspecciona tu documento de origen en lugar de
aceptar silenciosamente un documento reparado.
Solución de problemas comunes
Errores relacionados con la memoria
La comprobación de 100 MiB es una política de admisión de entradas, no una estimación de RAM. La
complejidad del PDF, la descompresión y las asignaciones de Java fuera del heap también importan.
-Xmx256m limita el heap de Java, no la memoria total del proceso, y no garantiza
que una entrada aceptada termine de procesarse. No hay agrupación automática por lotes: un documento
demasiado grande se rechaza, no se coloca en un lote demasiado grande ni se precede de un lote vacío.
Errores de acceso a archivos
Comprueba que las entradas y el JAR sean legibles y que el directorio padre de la salida exista y tenga permiso de escritura. Elige un destino nuevo si el indicado ya existe; el script de Python no lo elimina, aunque falle otro paso. No flexibilices indiscriminadamente los permisos de los archivos solo para que funcione una combinación. Si falla qpdf o PDFtk, ejecuta su comando localmente con la entrada problemática para inspeccionar el diagnóstico.
Para empezar, procesa una tarea a la vez. Por sí solos, unos archivos de entrada más pequeños no demuestran un consumo de memoria acotado ni una mayor tasa de procesamiento; añade concurrencia solo después de medir tu propia carga de trabajo y los recursos disponibles.
Consideraciones de seguridad
Manejo seguro de PDF
El modo encrypt_128bit de PDFtk
usa el algoritmo heredado RC4, no el moderno AES. No lo uses para proteger documentos confidenciales.
Una contraseña de propietario por sí sola no exige una contraseña para abrir un PDF; la contraseña
de usuario es el mecanismo independiente que exige una contraseña al abrirlo. Consulta las
opciones de contraseña cuando mantengas un flujo de trabajo heredado,
en lugar de copiar contraseñas en argumentos de comandos o scripts.
PROMPT permite introducir contraseñas de forma interactiva; este script de
combinación no interactivo no admite entradas protegidas con contraseña.
Gestiona los permisos de los archivos
Las restricciones de impresión y modificación de PDF dependen de que el lector las respete. No constituyen una barrera de control de acceso. Restringe el acceso a los propios archivos; el script de combinación no elimina texto ni metadatos sensibles, ni establece la política de almacenamiento y uso compartido de una organización.
Conclusión
Usa rangos de páginas explícitos cuando importe el orden, inspecciona el documento resultante y decide si es aceptable reemplazar una salida antes de automatizar el comando. El proyecto PDFtk Java es el lugar donde explorar operaciones adicionales una vez que este pequeño flujo de trabajo se ajuste a tus necesidades.
