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):

FormaEjemplo
Cabecera AuthorizationAuthorization: Bearer <TOKEN>
Cabecera propiaX-Soffice-Token: <TOKEN>
Parámetro en la URLhttps://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:

CampoTipoObligatorioDescripción
documentoficherosíDocumento DOCX, máximo 20 MB. El nombre del fichero es indiferente.
varstexto (JSON)noObjeto {"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": "..."}.

HTTPerrorCausa¿Reintentar?
400SIN_DOCUMENTOFalta el campo documento.No
400VARS_INCORRECTASvars no es un objeto JSON.No
400PETICION_INCORRECTAEl cuerpo no es multipart/form-data o está mal formado.No
401NO_AUTORIZADOToken ausente o incorrecto.No
413DOCUMENTO_DEMASIADO_GRANDESupera 20 MB.No
415NO_ES_DOCXEl fichero no es un DOCX (un .doc antiguo, un PDF, un ZIP cualquiera...).No
422CONVERSION_FALLIDALibreOffice no pudo convertir el documento (DOCX dañado o no soportado).Una vez como mucho
500ERROR_INTERNOFallo inesperado del servicio.Sí, con espera
503COLA_LLENAHay más de 30 conversiones esperando.Sí, con espera
504PLAZO_AGOTADOLa 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"}

Límites y tiempos

ConceptoValor
Tamaño máximo del DOCX20 MB
Conversiones simultáneas1; el resto espera turno
Conversiones en espera30 como máximo (después, 503)
Plazo por conversión60 s (después, 504)
Tiempo habitual1-2 s por documento de pocas páginas, más la espera en cola
Formatos de entradaSolo 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

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:

  1. Cambiar la URL por https://soffice.apps360.es/index.php?token=<TOKEN> (variable URLSOFFICE).
  2. 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.