«unoconv» para facilitar la conversión de documentos
Si tu aplicación aún depende de unoconv, empieza por comprobar su entorno Python/UNO y convertir un documento cuyo contenido conozcas. Esta guía abarca un nodo de trabajo existente con Debian 12, un script para procesar DOCX por lotes y las comprobaciones de PDF necesarias antes de aceptar el resultado.
Unoconv está en desuso y su repositorio está archivado. Para nuevas implementaciones, usa unoserver. Conserva esta guía para mantener un flujo de trabajo existente con unoconv mientras planificas esa migración.
Introducción a «unoconv» y sus capacidades de código abierto
Unoconv es una herramienta de línea de comandos de código abierto que invoca LibreOffice mediante sus bindings UNO. LibreOffice proporciona los filtros de importación y exportación; instalar únicamente un script de Python no permite convertir documentos. Las fuentes también afectan la disposición del contenido resultante.
Configuración e instalación de «unoconv» en tu sistema
Requisitos previos
Usa un nodo de trabajo dedicado al sistema heredado, con un entorno ya preparado y versiones
fijadas. El nodo probado usaba Debian 12 Bookworm, Python 3.11.2 de la distribución,
python3-uno compatible, unoconv 0.7 del paquete Debian 0.7-2,
LibreOffice Writer 7.4.7.2 y Liberation Sans de fonts-liberation2. Estas versiones
se probaron para el mantenimiento de sistemas heredados; esta página no recomienda instalar un
stack antiguo para una aplicación nueva.
El nodo necesita un perfil de usuario de LibreOffice y un directorio temporal con permisos de
escritura. Ejecuta un solo lote a la vez, sin una sesión de escritorio de LibreOffice ni un
listener compartido que use ese perfil. Usa documentos DOCX y ODT conocidos y confiables para
la primera conversión. Instala las fuentes que usan tus documentos reales antes de evaluar
la paginación. Los comandos de inspección siguientes requieren
poppler-utils de Poppler en el equipo donde inspecciones los PDF. Puede ser
el nodo de trabajo u otro equipo con acceso a los mismos archivos de salida.
Instalación en Linux (Ubuntu/Debian)
El paquete Bookworm de Debian proporciona esta versión
heredada de unoconv. Mantén juntos los paquetes de Python y UNO de la distribución. La siguiente
comprobación supone que python3 se resuelve como
/usr/bin/python3, fuera de un entorno virtual. Verifica cada comando por separado
y comprueba que este intérprete pueda importar UNO antes de la conversión:
(
for tool in python3 unoconv libreoffice fc-match; do
command -v "$tool" >/dev/null || { printf 'Missing %s\n' "$tool" >&2; exit 1; }
done
python3 -c 'import uno, unohelper; print(uno.__file__)' &&
python3 --version &&
unoconv --version &&
libreoffice --version &&
fc-match -f '%{family}\n' 'Liberation Sans'
)
En el nodo probado, UNO provenía de /usr/lib/python3/dist-packages/uno.py. Detente si falla la
importación o si las versiones indicadas difieren de las de la imagen verificada de tu despliegue.
Esta es una comprobación de un nodo existente, no una guía de instalación para las versiones
actuales de Ubuntu o Debian.
En el equipo donde inspeccionarás la salida, comprueba también las herramientas de PDF. Estas comprobaciones se realizaron con Poppler 26.08.0:
(
for tool in pdfinfo pdftotext pdftoppm; do
command -v "$tool" >/dev/null || { printf 'Missing %s\n' "$tool" >&2; exit 1; }
done
pdfinfo -v
)
Instalación en macOS
Homebrew indica que unoconv está deshabilitado. Instalar únicamente LibreOffice no establece una combinación funcional de unoconv y Python. Este tutorial no verifica una instalación en macOS; consulta las instrucciones de instalación del sucesor que sigue recibiendo mantenimiento cuando migres desde un nodo heredado de macOS.
Instalación en Windows
Las instalaciones nativas de Windows y las de WSL quedan fuera del alcance probado de esta guía. WSL no proporciona por sí mismo el Python compatible, los bindings UNO, las fuentes ni los filtros de LibreOffice. No consideres que cambiar a WSL resuelve los problemas de un entorno no verificado.
Guía paso a paso: convertir documentos de docx a PDF
Coloca un documento DOCX conocido en document.docx y un documento ODT conocido
en notes.odt, dentro de un directorio de trabajo. Registra sus cantidades
esperadas de páginas, encabezados, párrafos finales y cualquier tabla o gráfico. Estos comandos
directos escriben document.pdf y notes.pdf junto a los
archivos de entrada y pueden reemplazar PDF existentes, así que usa un directorio donde no
existan esos destinos:
unoconv -f pdf ./document.docx &&
unoconv -f pdf ./notes.odt
Inspecciona el resultado del DOCX con estos comandos. Escriben document.txt
y archivos PNG numerados, como document-page-1.png; repetir la comprobación reemplaza
esos archivos de inspección:
pdfinfo -box ./document.pdf &&
pdftotext -layout ./document.pdf ./document.txt &&
pdftoppm -cropbox -r 96 -png ./document.pdf ./document-page
Compara la cantidad de páginas y el texto extraído con el documento de origen, incluido el último
párrafo de cada página. Abre cada PNG renderizado y comprueba las tablas, los gráficos, los márgenes
y los saltos de página. Renderizar el CropBox muestra la página visible, de modo que el contenido
fuera de él no pueda ocultar un problema de recorte. Repite la inspección para
notes.pdf usando notes.txt y el prefijo
notes-page. Un archivo no vacío, una firma de PDF o un comando ejecutado
correctamente no demuestran que el documento correcto se haya convertido íntegramente.
Automatización de tareas de conversión de documentos con scripts
Para procesar un lote de DOCX de forma secuencial en esa instalación funcional, guarda lo siguiente
como convert_documents.py. Selecciona archivos regulares con el sufijo
.docx sin distinguir mayúsculas de minúsculas, omite directorios y enlaces
simbólicos, y requiere un directorio de salida nuevo. Su comprobación básica del archivo contenedor
reconoce los espacios de nombres del documento principal tanto de OOXML Transitional como de
OOXML Strict; no es un validador completo del esquema OOXML.
import os
from pathlib import Path
import posixpath
import subprocess
import sys
from tempfile import TemporaryDirectory
from urllib.parse import unquote
from xml.etree import ElementTree
from zipfile import BadZipFile, ZipFile
import zlib
def check_docx(path):
relationship_types = {
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
'http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument',
}
document_tags = {
'{http://schemas.openxmlformats.org/wordprocessingml/2006/main}document',
'{http://purl.oclc.org/ooxml/wordprocessingml/main}document',
}
with ZipFile(path) as archive:
if archive.testzip() is not None:
raise ValueError('Damaged DOCX member')
relationships = ElementTree.fromstring(archive.read('_rels/.rels'))
main_parts = [item for item in relationships
if item.get('Type') in relationship_types
and item.get('TargetMode', 'Internal') == 'Internal']
if len(main_parts) != 1:
raise ValueError('Missing main document relationship')
part = posixpath.normpath(unquote(main_parts[0].get('Target', '')).lstrip('/'))
types = ElementTree.fromstring(archive.read('[Content_Types].xml'))
content_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'
if not any(item.get('PartName') == '/' + part
and item.get('ContentType') == content_type for item in types):
raise ValueError('Not a DOCX main part')
if ElementTree.fromstring(archive.read(part)).tag not in document_tags:
raise ValueError('Not a Word document')
def convert_documents(input_dir, output_dir):
source = Path(input_dir).resolve(strict=True)
files = sorted(path for path in source.iterdir()
if not path.is_symlink() and path.is_file()
and path.suffix.lower() == '.docx')
if not files:
print('No DOCX files found', file=sys.stderr)
return 1
output = Path(output_dir).absolute()
output.mkdir(mode=0o700)
failures = 0
for path in files:
try:
check_docx(path)
with TemporaryDirectory(prefix='.conversion-', dir=output) as temporary:
candidate = Path(temporary) / 'result.pdf'
subprocess.run(
['unoconv', '-f', 'pdf', '-o', str(candidate), str(path)],
check=True, timeout=120,
)
if not candidate.is_file() or candidate.stat().st_size == 0:
raise ValueError('No nonempty PDF was produced')
# Hard-link publication fails instead of replacing a destination that appeared.
os.link(candidate, output / (path.stem + '.pdf'))
print(f'Converted {path.name}')
except (OSError, ValueError, KeyError, BadZipFile, ElementTree.ParseError,
zlib.error, NotImplementedError, RuntimeError, subprocess.SubprocessError):
failures += 1
print(f'Conversion failed: {path.name}', file=sys.stderr)
return 1 if failures else 0
if __name__ == '__main__':
if len(sys.argv) != 3:
sys.exit('Usage: python3 convert_documents.py <input-directory> <new-output-directory>')
try:
sys.exit(convert_documents(sys.argv[1], sys.argv[2]))
except (OSError, ValueError):
sys.exit('Cannot prepare conversion directories; use an existing input and a new output directory')
Ejecútalo con tus archivos DOCX en docs y sin que exista el directorio
new-pdfs:
python3 convert_documents.py docs new-pdfs
El script intenta procesar los archivos posteriores tras un fallo y devuelve el estado 1 si falla algún archivo. Informa del nombre del archivo que falla, elimina los candidatos temporales y conserva los destinos existentes, incluidas las colisiones entre nombres con la misma base. Los PDF generados correctamente siguen disponibles cuando falla otro archivo. Inspecciónalos con las comprobaciones de texto y renderizado anteriores antes de usarlos. La publicación solo significa que el convertidor terminó correctamente y produjo un candidato no vacío.
Para reintentar, conserva los PDF publicados anteriormente y usa otro directorio de salida nuevo. Una selección vacía devuelve el estado 1 sin crear el directorio de salida. El tiempo límite de 120 segundos limita la duración de cada proceso cliente; no garantiza que LibreOffice haya dejado de trabajar. Un nodo interrumpido puede dejar directorios de preparación, así que inspecciona el almacenamiento que le pertenece antes de reintentar. Este script está destinado a un lote local confiable, no a un endpoint de subida de archivos no confiables. Un nodo de ese tipo necesita su propia supervisión de procesos, límites de recursos y aislamiento del sistema de archivos.
Solución de problemas comunes
Conflictos de versiones de Python
Si falla la comprobación de importación de UNO, verifica qué Python se está ejecutando y si coincide con el puente UNO instalado. El proyecto original documenta este requisito del intérprete. No basta con cualquier entorno virtual: un venv predeterminado oculta el módulo UNO de la distribución. Cambiar únicamente la versión de Python no instala un puente compatible.
Compatibilidad de versiones de LibreOffice
El unoconv de Debian probado emite una advertencia de desuso de distutils
en Python 3.11; incluso si la conversión termina correctamente, sigue siendo necesario comprobar
los PDF resultantes. Conserva la combinación verificada de Python/UNO/LibreOffice en la imagen
de tu sistema heredado. Si falla un documento, intenta abrirlo y exportarlo en LibreOffice para
distinguir un problema de importación o exportación de uno en la ruta del cliente unoconv.
Consideraciones para el despliegue en contenedores
Un contenedor debe proporcionar directorios de perfil, caché, datos, ejecución y archivos temporales
con permisos de escritura para el usuario que realiza la conversión. Crea los directorios antes
de iniciar LibreOffice al configurar XDG_CONFIG_HOME,
XDG_CACHE_HOME, XDG_DATA_HOME o
XDG_RUNTIME_DIR. La ausencia de un perfil con permisos de escritura puede impedir
el inicio antes de la conversión del documento. Mantén las tareas secuenciales y detén todo el
nodo de trabajo bajo tu control cuando un cliente supere el tiempo límite; terminar únicamente
el cliente no demuestra que se haya completado la limpieza.
Alternativas modernas y comparación
| Despliegue | Opción |
|---|---|
| Nodo de trabajo heredado existente con versiones fijadas | Mantén unoconv mientras pruebas documentos representativos. |
| Nuevo despliegue | Usa unoserver. |
Unoserver (recomendado)
Unoserver separa un nodo de trabajo persistente de LibreOffice de su cliente de conversión. Su servidor sigue necesitando un intérprete de Python que pueda importar UNO; no elimina esa dependencia. Sigue las instrucciones de instalación de unoserver y luego consulta la conversión de documentos desde Rust para ver la integración que sigue recibiendo mantenimiento. Las pruebas de esta página abarcan unoconv, no ese flujo de trabajo independiente con servidor.
CLI de LibreOffice
Para una conversión puntual, LibreOffice también ofrece una
opción de exportación por línea de comandos.
En el nodo Debian probado, esto escribe document.pdf en el directorio actual:
libreoffice --headless --convert-to pdf ./document.docx
Usa un destino que no exista y aplica la misma inspección del PDF. LibreOffice puede reemplazar un PDF existente, y un estado de salida por sí solo no permite determinar el contenido resultante. Esta vía evita el cliente unoconv, pero sigue usando los filtros y las fuentes de LibreOffice.
Soluciones basadas en la nube
Si quieres interactuar mediante una API en lugar de operar un nodo de trabajo de ofimática, evalúa el servicio de conversión de documentos de Transloadit con tus documentos representativos. Es una integración independiente; los comandos locales anteriores no prueban un servicio alojado.
