Averigua qué está instalado de verdad
Los motores son opcionales y se resuelven en tiempo de ejecución. Esto reporta qué encontró, en qué versión y qué falta, para que una máquina pueda decidir si un trabajo siquiera es posible antes de empezarlo.
PyPIApache-2.0sin nada AGPL ni GPL
Todas las tareas comunes de PDF. Una sola CLI segura. Sin copyleft debajo.
Instala pdf-tooling; el comando es pdftooling.
# Instalación (publicado en PyPI)
uv tool install pdf-tooling
# o bien: pip install pdf-toolingLinux y macOS. macOS 15 o posterior en Apple silicon; las Mac con Intel no son compatibles.
Una corrida en seco planea la unión completa de tres archivos; después ls es la comprobación, y no se escribió nada.
Publicado en PyPI como pdf-tooling: todavía no hay fórmula de Homebrew, ni paquete del sistema, ni imagen de contenedor. Todo comando que imprime pdftooling --help ya viene incluido; consulta Comandos.
§01 / Seguridad
Cuatro garantías, verificadas por la propia suite de pruebas del producto en lugar de prometidas.
Planea y reporta; no escribe nada, en ningún lado. Úsalo primero con cualquier cosa de la que no estés seguro: no queda un estado a medio escribir que limpiar después, porque no se escribió nada.
Las salidas nunca sobrescriben. Una corrida destructiva en lote sin --force se rechaza con código 5 en lugar de preguntar, así que un script y una persona reciben la misma garantía: no hay una confirmación interactiva que un llamador automatizado pueda satisfacer por accidente.
Cada escritura va a un archivo temporal en el mismo sistema de archivos del destino, se sincroniza con fsync y luego se mueve a su lugar con un rename atómico. Las entradas nunca se modifican a menos que pidas --in-place, que primero deja un respaldo .bak junto al archivo.
argv se puede leer desde /proc y queda en el historial de la terminal, así que nunca se acepta una contraseña como valor de línea de comandos: no existe --password, ni --user-password, ni --owner-password. Léela de un archivo o de stdin.
Los bits de permisos son informativos, no una restricción. Son una petición al lector, no un candado. El cifrado protege el contenido; los bits por sí solos no protegen nada.
§02 / Arquitectura
Casi todas las herramientas te piden confiar en una lista larga de dependencias. Esta está hecha para que no tengas que hacerlo. Cada motor de PDF que usa queda detrás de 8 módulos adaptadores, archivos chicos cuyo único trabajo es hablar con un motor. Solo 1 archivo más, adapters/subprocess_util.py, puede lanzar otro programa, y nada más en todo el código tiene permiso de hacerlo. Son 9 archivos en total entre tú y cualquier motor, así que leerlos todos toma una tarde y no un proyecto. Escribir en tu disco está igual de acotado, y tests/test_import_boundaries.py hace fallar la compilación el día que cualquiera de los dos límites deje de cumplirse.
El diagrama de abajo muestra cómo está acomodado. No necesitas nada de esto para usar la herramienta.
src/pdf_tooling/cli/La única capa que puede importar el framework de CLI.
Interpreta las banderas, construye un solo OperationPlan inmutable y elige un renderizador.
src/pdf_tooling/ops/Funciones simples, sin framework, que trabajan solo a través de los puertos de abajo. Aquí nunca se decide un rechazo del sistema de archivos.
Sin entrada/salida salvo a través de Safety y Ports.
Ops llega a Safety y a Ports en paralelo.
src/pdf_tooling/safety/El único lugar que escribe en disco: un archivo, no un paquete. Las dos listas de permitidos de escritura están vacías.
Escritura a temporal, fsync y rename atómico. Aquí vive la compuerta de --dry-run.
src/pdf_tooling/ports/Solo definiciones de typing.Protocol. Importar todos los módulos de puertos no carga ningún motor.
StructureEngine · RasterEngine · ComposeEngine · TextEngine · OcrEngine · OfficeConverter
src/pdf_tooling/adapters/El único paquete que puede importar una biblioteca de motor. subprocess_util.py es el único módulo del árbol que puede lanzar un proceso.
Ocho módulos sirven a los seis puertos de arriba. El noveno archivo es el que tiene permiso de lanzar un programa, no el respaldo de un motor.
Frontera que se hace cumplir
tests/test_import_boundaries.pyarriba: sin importar motores, sin lanzar programas, sin escribir fuera del único archivo con permiso · abajo: todos los motores de terceros
pypdf · pypdfium2 · pikepdf · reportlab · pdfplumber · Tesseract · LibreOffice
No son parte de este código. Solo licencias permisivas, verificadas en CI.
src/pdf_tooling/output/stdout es la carga útil; stderr es todo lo demás.
Lo que devuelve Ops, renderizado como table, json o ndjson.
▣ marca un archivo que es el único con permiso para hacer su trabajo. Cada regla de frontera la hace cumplir la prueba que aparece junto a ella, y el riel corre en un solo sentido: hacia abajo.
| Capa | Ruta | La regla | La hace cumplir |
|---|---|---|---|
| L1 | src/pdf_tooling/cli/ | La única capa que puede importar el framework de CLI. | test_no_typer_or_click_import_below_l1 |
| L2 | src/pdf_tooling/ops/ | Funciones simples, sin framework, que trabajan solo a través de los puertos de abajo. Aquí nunca se decide un rechazo del sistema de archivos. | test_no_ops_module_names_a_filesystem_tier_refusal_class |
| L3 | src/pdf_tooling/safety/ | El único lugar que escribe en disco: un archivo, no un paquete. Las dos listas de permitidos de escritura están vacías. | test_write_calls_inside_safety_are_confined_to_the_chokepoint · test_both_allowlists_are_empty |
| L4 | src/pdf_tooling/ports/ | Solo definiciones de typing.Protocol. Importar todos los módulos de puertos no carga ningún motor. | test_importing_the_port_layer_loads_no_engine |
| L5 | src/pdf_tooling/adapters/ | El único paquete que puede importar una biblioteca de motor. subprocess_util.py es el único módulo del árbol que puede lanzar un proceso. | test_no_engine_library_is_imported_outside_adapters · test_nothing_outside_the_chokepoint_can_spawn |
| L6 | src/pdf_tooling/output/ | stdout es la carga útil; stderr es todo lo demás. | README.md ## Output contract |
Tus archivos
Mata el proceso, pierde el disco, desconecta la corriente: en otras herramientas el resultado habitual es un archivo truncado donde antes estaba tu documento. Aquí cada paso ocurre en un archivo de trabajo junto al destino, y tu propio archivo se toca exactamente una vez, al final, con un rename que el sistema operativo no puede dejar a medias. Nada más en toda la corrida puede alcanzarlo. Para eso sirve la única ruta de escritura de abajo: un solo archivo de este código tiene permiso de escribir (src/pdf_tooling/safety/atomic.py), y por eso la garantía aplica a todos los comandos y no nada más a los que alguien se acordó de hacer seguros.
Rechaza un destino que ya existe si no pasas --force. Código 5.
Regresa antes de cualquier llamada al sistema de archivos, en ambos modos.
Un archivo temporal junto al destino, en el mismo sistema de archivos: eso es lo que hace atómico el rename.
Escribir, vaciar el búfer, fsync.
Solo con --in-place.
aquí cambia tu archivo
Atómico. El único punto donde las dos vías se tocan.
Si algo falla en los pasos ① a ⑤: el temporal se elimina, tu archivo queda intacto y el proceso termina con un código distinto de cero. Una terminación forzada (SIGKILL) no se puede atender: puede dejar el temporal oculto, y tu archivo sigue intacto.
Todo lo que pasa antes del cruce sucede en un archivo de trabajo, en el mismo sistema de archivos que el destino; solo la última estación toca tu archivo, y lo hace de forma atómica.
§03 / Comandos
Lo que de otro modo armarías con varias herramientas distintas, con una sola forma de decir qué páginas quieres. Todo comando que direcciona páginas acepta la misma gramática de rangos: 1-3,5,9-, even, odd, first, last, -1, !5. La aprendes una sola vez. Esta lista se verifica contra pdftooling --help en cada versión, así que no puede desviarse de la herramienta que instalaste.
Reporta el adaptador resuelto de cada puerto, su versión y una sugerencia de instalación acorde a tu sistema para lo que falte.
Número y tamaño de páginas, cifrado y bits de permisos, metadatos, productor, lista de fuentes, presencia de firma y rotación por página.
Reporta las versiones de la herramienta, del runtime y de los motores.
Concatena PDFs; selección de páginas por entrada con ruta:rango; una entrada de índice por archivo de origen.
De un PDF a muchos, por tamaño fijo de bloque, por rangos explícitos, página por página o en los marcadores de primer nivel.
Escribe las páginas seleccionadas en un PDF nuevo, en el orden indicado.
Escribe todo excepto las páginas seleccionadas.
Rota las páginas seleccionadas en múltiplos de 90°, de forma absoluta o relativa.
Reescribe el orden de las páginas a partir de una secuencia explícita; se permiten duplicados y se respeta el orden dado.
Recuperación estructural de un PDF dañado o mal formado con el analizador de recuperación de libqpdf.
Reescribe para entrega progresiva por bytes ("vista web rápida").
Reporta los bits de permisos y el algoritmo de cifrado de un PDF cifrado.
Lee el diccionario de información del documento y el XMP.
Escribe o limpia campos de información del documento y el XMP.
De PDF a PNG/JPEG/TIFF/WEBP al DPI o ancho en píxeles que elijas.
De imágenes a PDF. Las entradas JPEG se incrustan como flujos DCTDecode (conservan los bytes, sin recodificar).
De texto (v1) a PDF. Markdown y HTML detrás del extra [html] (fase 2).
Extrae texto: ruta rápida (pdfium) o con reconocimiento de maquetación (pdfplumber), con geometría por bloque.
Detecta y extrae tablas a CSV o JSON.
Reduce el tamaño: flujos de objetos de libqpdf más recompresión de flujos (sin pérdida) y, opcionalmente, reducción y recompresión de imágenes con Pillow (con pérdida).
Aplica AES-256 (o RC4-128 con --legacy) con contraseñas de usuario y de propietario, y un conjunto de permisos.
Quita el cifrado cuando se da la contraseña correcta.
Superpone o subpone texto generado sobre las páginas seleccionadas.
Superpone o subpone una página de un PDF existente sobre las páginas seleccionadas.
Agrega una capa de texto de Tesseract sobre los píxeles intactos: se genera un PDF solo de texto por página y se une.
De Office a PDF con LibreOffice en modo headless.
Corre pdftooling --help para ver el árbol completo.
§04 / Ejemplos
Cuatro sesiones reales, capturadas de la herramienta en lugar de escritas para esta página.
Hecha para agentes, no solo para personas
Un agente que maneja una herramienta de línea de comandos tiene que adivinar dos veces: si la salida que acaba de leer significa lo que cree, y si lo que pidió de verdad ocurrió. Aquí se quitan las dos adivinanzas. Toda ruta de lectura emite un schema_version declarado y se convierte en JSON por su cuenta cuando stdout no es una terminal. Toda falla es un objeto estructurado cuyo code es el mismo número con el que termina el proceso, tomado de una tabla congelada como API pública, así que un plan puede ramificar sobre un entero en lugar de reconocer texto en inglés. Y --dry-run reporta exactamente lo que haría una corrida real sin escribir nada, en ningún lado, lo que convierte planear-verificar-actuar en una secuencia real y no en una esperanza.
Los motores son opcionales y se resuelven en tiempo de ejecución. Esto reporta qué encontró, en qué versión y qué falta, para que una máquina pueda decidir si un trabajo siquiera es posible antes de empezarlo.
No se pasó ninguna bandera. La salida es JSON porque stdout no es una terminal: el mismo comando es legible para una persona cuando lo corres tú, y analizable cuando lo corre un script.
Un renglón por entrada, cada uno con su propio código de salida y su conteo de bytes. No se escribe nada hasta que se leyeron todas las entradas y se verificó el destino.
Córrelo otra vez y se rehúsa en lugar de sobrescribir. El rechazo es un objeto estructurado con un código estable: no es un mensaje que haya que reconocer con expresiones regulares, ni una sobrescritura silenciosa.
§05 / Códigos de salida
7 códigos de salida que significan lo mismo en todos los comandos, más las formas en las que se renderiza -o table / -o json / -o ndjson. Los dos son API pública, los dos están respaldados por una prueba, y ninguno se escribió a mano en esta página.
| Código | Nombre | Significado |
|---|---|---|
| 0 | OK | Éxito, incluido un reporte vacío pero válido. Un --dry-run refleja el código que devolvería la corrida real, así que no siempre es 0. |
| 1 | FAILURE | La operación se ejecutó y falló: entrada corrupta, error del motor o un destino donde no se puede escribir. |
| 2 | USAGE | Invocación incorrecta: bandera desconocida, banderas mutuamente excluyentes, rango de páginas mal formado o subcomando desconocido. |
| 3 | ENGINE_MISSING | Falta un motor o un binario requerido. El mensaje siempre trae una sugerencia de instalación. |
| 4 | NO_INPUT | Invocación válida, pero no hay nada sobre lo cual actuar. |
| 5 | REFUSED | Una compuerta de seguridad lo rechazó. |
| 6 | AUTH | Falta la contraseña, es incorrecta o es del tipo equivocado. |
| Agrega un código; nunca renumeres uno. Romper esta tabla es un cambio de versión mayor, no un parche. | ||
Los errores son la única asimetría deliberada: con -o table un error es un mensaje de una línea en stderr, pero con -o json o -o ndjson es un objeto en stdout, así que un consumidor automatizado que lee stdout nunca tiene que leer además stderr para enterarse de que una corrida falló.
§06 / Licencias
Apache-2.0, y todos los motores que tiene debajo son BSD, MIT, Apache o MPL: nada AGPL ni GPL en el grafo de llamadas.
La garantía es un trabajo de CI y una prueba unitaria, no una promesa en un README: la compuerta de licencias hace fallar la compilación ante cualquier paquete AGPL, GPL o LGPL en la cerradura distribuida, y una prueba a nivel de código recorre todos los archivos bajo src/ buscando importaciones prohibidas y llamadas externas con subprocess: la fuga realista que un escaneo de licencias a nivel de paquete no puede ver por sí solo.
Calculado directo del mismo inventario generado que publica el inventario completo: la cerradura de instalación por omisión son 21 paquetes, el extra opcional [html] agrega 10 más, 31 paquetes en total.
pikepdf incluye libqpdf bajo MPL-2.0: copyleft a nivel de archivo, usado sin modificar como dependencia y compatible con Apache sobre esa base. Está documentado en NOTICE y en el inventario completo THIRD_PARTY_LICENSES.
Nada en la instalación por omisión lleva una licencia de la familia GPL, y el único renglón de todo el inventario que nombra una es opcional y ofrece MPL 1.1 como alternativa: pyphen 0.18.1 entra únicamente a través del extra opcional [html], con licencia triple GPLv2+, LGPLv2+ o MPL 1.1, a elección de quien lo toma.
Los 31 paquetes, con licencias, niveles y funciones →
Inventario completo: THIRD_PARTY_LICENSES.
Postura de producción
Tres promesas, y cómo comprobar cada una.
schema_version, así que un script que escribas hoy sigue funcionando en todas las versiones menores y de parche.Instálala
La misma instalación con la que abrió esta página: cópiala, córrela y comprueba lo demás por tu cuenta.
uv tool install pdf-toolingApache-2.0. Las formas de salida estructurada y la tabla de códigos de salida son API pública. Los errores y sus correcciones viven en los issues de GitHub y en el changelog. Las contribuciones son bienvenidas, bajo el DCO.
el código de salida coincide con la carga útil