soffice
Microservicio de conversión de documentos DOCX → PDF con LibreOffice · versión 2.1.0
Recibe un documento Word (.docx), opcionalmente sustituye variables en su texto, lo convierte a PDF con LibreOffice y devuelve el PDF en la misma respuesta. Atiende las peticiones por orden de llegada, así que varias aplicaciones pueden usarlo a la vez sin recibir PDF vacíos.
Inicio rápido
curl -f -o resultado.pdf \
-H "Authorization: Bearer <TOKEN>" \
-F documento=@plantilla.docx \
https://soffice.apps360.es/index.php
En la aplicación, la URL y el token van en el .env:
URLSOFFICE=https://soffice.apps360.es/index.php?token=<TOKEN>
Autenticación
Cada aplicación tiene su propio token, que entrega el administrador del servicio. Se envía de una de estas formas (por orden de preferencia):
| Forma | Ejemplo |
|---|---|
Cabecera Authorization | Authorization: Bearer <TOKEN> |
| Cabecera propia | X-Soffice-Token: <TOKEN> |
| Parámetro en la URL | https://soffice.apps360.es/index.php?token=<TOKEN> |
El parámetro en la URL existe para clientes que solo pueden configurar una URL. El servicio no guarda los parámetros en sus logs, pero el cliente sí podría: no registrar la URL completa. Solo se admite HTTPS; las peticiones por HTTP se redirigen.
Convertir un documento
POST/index.php (equivalentes: / y /convertir)
Cuerpo multipart/form-data:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
documento | fichero | sí | Documento DOCX, máximo 20 MB. El nombre del fichero es indiferente. |
vars | texto (JSON) | no | Objeto {"clave": "valor"} con las sustituciones. Ver Variables. |
Respuesta correcta: 200, Content-Type: application/pdf y el PDF en el cuerpo.
Un 200 siempre lleva un PDF válido.
Respuestas y errores
Cualquier respuesta distinta de 200 es un error y lleva un JSON {"error": "CODIGO", "mensaje": "..."}.
| HTTP | error | Causa | ¿Reintentar? |
|---|---|---|---|
| 400 | SIN_DOCUMENTO | Falta el campo documento. | No |
| 400 | VARS_INCORRECTAS | vars no es un objeto JSON. | No |
| 400 | PETICION_INCORRECTA | El cuerpo no es multipart/form-data o está mal formado. | No |
| 401 | NO_AUTORIZADO | Token ausente o incorrecto. | No |
| 413 | DOCUMENTO_DEMASIADO_GRANDE | Supera 20 MB. | No |
| 415 | NO_ES_DOCX | El fichero no es un DOCX (un .doc antiguo, un PDF, un ZIP cualquiera...). | No |
| 422 | CONVERSION_FALLIDA | LibreOffice no pudo convertir el documento (DOCX dañado o no soportado). | Una vez como mucho |
| 500 | ERROR_INTERNO | Fallo inesperado del servicio. | Sí, con espera |
| 503 | COLA_LLENA | Hay más de 30 conversiones esperando. | Sí, con espera |
| 504 | PLAZO_AGOTADO | La conversión tardó más de 60 s. | Una vez como mucho |
Variables
Si se envía vars, antes de convertir se sustituye cada clave por su valor en el cuerpo del
documento (word/document.xml), en el orden del JSON. Es un reemplazo de texto literal:
vars = {"[NOMBRE DEL CLIENTE]": "Ana García", "[FECHA]": "28/10/2026"}
- La clave tiene que estar escrita de una sola vez en Word. Si Word la partió en varios fragmentos (por ejemplo, por cambiar el formato a mitad o por la corrección ortográfica), no se encuentra. Para plantillas complejas es más fiable rellenarlas en la aplicación (docxtemplater, PhpWord...) y enviar el DOCX ya relleno.
- Los valores se insertan sin escapar, igual que en el NOffice anterior. Escapar
&,<y>en el cliente (en PHP,htmlspecialchars); si no, el documento queda dañado y se recibe un 422. - Solo se tratan el cuerpo del documento, no las cabeceras ni los pies de página.
Límites y tiempos
| Concepto | Valor |
|---|---|
| Tamaño máximo del DOCX | 20 MB |
| Conversiones simultáneas | 1; el resto espera turno |
| Conversiones en espera | 30 como máximo (después, 503) |
| Plazo por conversión | 60 s (después, 504) |
| Tiempo habitual | 1-2 s por documento de pocas páginas, más la espera en cola |
| Formatos de entrada | Solo DOCX |
Si el cliente cierra la conexión, su conversión se descarta. El servicio no guarda los documentos: se borran al terminar cada conversión.
Ejemplos
Node.js 18 o superior (sin dependencias)
import fs from 'node:fs/promises';
import path from 'node:path';
export async function convertirDocxToPdf(rutaDocx, rutaPdf) {
const formulario = new FormData();
formulario.append('documento', new Blob([await fs.readFile(rutaDocx)]), path.basename(rutaDocx));
// Opcional: formulario.append('vars', JSON.stringify({ '[NOMBRE]': 'Ana' }));
const respuesta = await fetch(process.env.URLSOFFICE, {
method: 'POST',
body: formulario,
signal: AbortSignal.timeout(300_000),
});
if (!respuesta.ok) {
const { error, mensaje } = await respuesta.json().catch(() => ({}));
throw new Error(`soffice respondió ${respuesta.status} ${error ?? ''}: ${mensaje ?? ''}`);
}
const pdf = Buffer.from(await respuesta.arrayBuffer());
if (pdf.subarray(0, 5).toString('latin1') !== '%PDF-') {
throw new Error('soffice no devolvió un PDF');
}
await fs.writeFile(rutaPdf, pdf);
return rutaPdf;
}
Node.js con el paquete form-data (como en los clientes actuales)
const formData = new FormData(); // import FormData from 'form-data'
formData.append('documento', fs.createReadStream(pathDocx));
formData.submit(process.env.URLSOFFICE, (err, res) => {
// Comprobar res.statusCode === 200 antes de escribir el cuerpo en el .pdf
});
PHP
function convertirDocxToPdf(string $rutaDocx, string $rutaPdf, ?array $vars = null): void
{
$campos = ['documento' => new CURLFile($rutaDocx)];
if ($vars !== null) {
$campos['vars'] = json_encode(array_map('htmlspecialchars', $vars));
}
$ch = curl_init(getenv('URLSOFFICE'));
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $campos,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 300,
]);
$cuerpo = curl_exec($ch);
$codigo = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($codigo !== 200 || strncmp($cuerpo, '%PDF-', 5) !== 0) {
throw new RuntimeException("soffice respondió $codigo: $cuerpo");
}
file_put_contents($rutaPdf, $cuerpo);
}
Buenas prácticas en el cliente
- Pasar siempre por una única función (p. ej.
convertirDocxToPdf), nunca llamar al servicio desde varios sitios. - Comprobar el código HTTP y que el cuerpo empieza por
%PDF-antes de guardar o enviar el PDF. No escribir en el .pdf la respuesta de un error. - Plazo del cliente de al menos 300 s: la petición puede esperar turno en la cola.
- Reintentar solo 500, 503 y errores de red, con espera creciente (5 s, 15 s, 45 s). No reintentar los 4xx: el resultado será el mismo.
- Token en el
.env, nunca en el código ni en el repositorio. - Si las pruebas dan «no se ha podido generar el PDF», comprobar primero que el
.envtieneURLSOFFICE.
Estado del servicio
GET/salud (sin token)
{"estado":"ok","enCurso":0,"enEspera":0}
GET/openapi.json: contrato de la API en formato
OpenAPI 3.1, para generar clientes o importarlo en Postman.
Migrar desde NOffice
La API es la misma que la del NOffice en PHP (mismos campos y misma ruta /index.php). Para migrar:
- Cambiar la URL por
https://soffice.apps360.es/index.php?token=<TOKEN>(variableURLSOFFICE). - Asegurarse de que el cliente trata como error cualquier respuesta distinta de 200: el NOffice devolvía 200 incluso cuando fallaba; soffice devuelve el código real.