Convertir documento MS Office a PDF

Algo que siempre he estado buscando, creo que he conseguido encontrarlo.

No es perfecto, puesto que sólo funciona en Windows, pero la conversión que hace sí que es PERFECTA, ya que utiliza el software de Microsoft para hacer la conversión de cualquier documento de Microsoft Office a PDF.

Para hacer la conversión utiliza los productos MS instalados en un Windows y para ejecutar el software de Microsoft en background utilizamos el producto gratis de la empresa Cognidox  “office_to_pdf.exe”.

Este producto se puede descargar desde https://github.com/cognidox/OfficeToPDF

La documentación de sus requisitos de instalación y sus parámetros lo podéis obtener en la misma dirección. Por favor, revisar los requisitos de instalación.

El ejemplo que he hecho consta de 2 partes:

  • server_pdf. Aplicación PHPRunner que gestiona una única tabla en donde se registra las conversiones que se hacen y con ese dato, se informa del nombre del fichero, tamaño y tiempo que ha durado la conversión.
    Esta es la parte que tiene que ser ejecutada en una máquina Windows con el producto de MS Office instalado.

  • client_pdf. Aplicación de una única tabla en donde se “sube” un fichero de MS office. Una vez que está subido se solicita, mediante botón, la conversión a formato PDF.
    Puede estar en Linux o cualquier otro S.O. donde se ejecute PHP.

El ejemplo sólo muestra la capacidad de conversión de los documentos de MS Office a PDF, ahora bien, cada uno que lo utilice podrá hacerlo para aplicaciones funcionalmente  muy diferentes.

DEMO: https://fhumanes.com/client_pdf

Sólo estará utilizable cuando esté puesto mi PC con el proceso de “server_pdf”, ya que estas páginas están alojadas en máquinas Linux.

Usuario: “admin”/”admin”

Como recordaréis, disponemos de varios ejemplos para construir de forma bastante fácil documentos Word y Excel desde aplicaciones PHPRunner, así pues esos documentos, una vez elaborados se pueden convertir, a través de la funcionalidad de este ejemplo, en PDF.

El hecho de separar o desacoplar el servicio de conversión de PDF es muy importante, dado que este sistema requiere una máquina Windows (con MS Office instalado) y es un proceso muy pesado (exige muchos recursos de la máquina para hacer la conversión). El separarlo hace que el aplicativo que está sirviendo al cliente no se “resienta” con estos procesos de conversión de formato de ficheros.

Para comunicarse estas 2 aplicaciones hemos utilizado el protocolo de RestFull API y os muestro el código más relevante de la parte Cliente y Servidor:

convert_pdf.php

<?php
/*
$data = $button->getCurrentRecord();

$id_record = $data['id_client_pdf_file'];  // 
// get information about  files
$fileArray = my_json_decode($data["files"]);
if (count($fileArray) <> 1){ //  To convert it is necessary that there is only one file
  $result['error'] = "No se ha realizado conversión";
} else {
    $result['error'] = "";
    include "MyCode/convert_pdf.php";		 // Convert file to PDF
}
*/
$file = $fileArray[0]['name'];
$name = $fileArray[0]['usrName'];
require_once __DIR__ . '/unirest_3.0.4/autoload.php'; // Load Library Unirest
$pathFile = __DIR__ . "/../".$file;

$document = file_get_contents($pathFile);  // Charge the content of the file from its physical location

$headers = array(
    'Authorization' => '3d524a53c110e4c22463b10ed32cef9d',
    'Accept' => 'application/json'
    );
$data = array('name' => $name, 'file' => base64_encode($document), 'option' => '/readonly /noquit /excel_active_sheet');
$body = Unirest\Request\Body::multipart($data);
// $body = Unirest\Request\Body::form($data);
$domainHost="http://localhost";
$response = Unirest\Request::post($domainHost.'/server_pdf/restapi/v1/document', $headers, $body);
$code = $response->code;        // HTTP Status code
$headers = $response->headers;     // Headers

$data_api =  $response->raw_body;    // Unparsed body
$data_api = json_decode($data_api,true);
if ($data_api['error'] == true || $code <> '201'){
   $result['error'] = 'El servidor de conversión PDF ha dado error';
   return;
}
$name = $data_api['document']['name'];
$document = base64_decode($data_api['document']['file']);
$part_path = pathinfo($file);
$file = $part_path['dirname'].'/'.$part_path['filename'].'.pdf';
$pathFile = __DIR__ . "/../".$file;
// Wirte new file PDF
file_put_contents($pathFile, $document);

$sizeFile = filesize($pathFile);
// Create data of new file
$fileArray[1]['name']=$file;
$fileArray[1]['usrName']=$name;
$fileArray[1]['size']=$sizeFile;
$fileArray[1]['type']='application/pdf';
$fileArray[1]['searchStr']=$name;

$field = my_json_encode($fileArray);

$dataDB = array();
$keyvalues = array();
$dataDB["files"] = $field;
$keyvalues["id_client_pdf_file"] = $id_record;
DB::Update("client_pdf_file", $dataDB, $keyvalues );
?>

 

index.php

<?php
/**
 *
 * @About:      API Interface
 * @File:       index.php
 * @Date:       $Date:$ Nov-2015
 * @Version:    $Rev:$ 1.0
 * @Developer:  Federico Guzman (federicoguzman@gmail) and modified Fernando Humanes ([email protected])

/* Los headers permiten acceso desde otro dominio (CORS) a nuestro REST API o desde un cliente remoto via HTTP
 * Removiendo las lineas header() limitamos el acceso a nuestro RESTfull API a el mismo dominio
 * Nótese los métodos permitidos en Access-Control-Allow-Methods. Esto nos permite limitar los métodos de consulta a nuestro RESTfull API
 * Mas información: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
 **/
header("Access-Control-Allow-Origin: *");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: PUT, GET, POST, DELETE, OPTIONS');
header("Access-Control-Allow-Headers: X-Requested-With");
header('Content-Type: text/html; charset=utf-8');
header('P3P: CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"'); 

include_once '../include/Config.php';

require_once("../../include/dbcommon.php"); // DataBase PHPRunner

/* Puedes utilizar este file para conectar con base de datos incluido en este demo; 
 * si lo usas debes eliminar el include_once del file Config ya que le mismo está incluido en DBHandler 
 **/
//require_once '../include/DbHandler.php'; 

require '../libs/Slim/Slim.php'; 
\Slim\Slim::registerAutoloader(); 
$app = new \Slim\Slim();



/* Usando POST para convertir fichero */

$app->post('/document', 'authenticate', function() use ($app) {
    // check for required params
    $startConvert = date("Y-m-d H:i:s");;                                  // marcar fecha de inicio
    verifyRequiredParams(array('name', 'file'));

    $BlobInput=base64_decode($app->request->post('file'));  // Convert Base64
    // $BlobInput=$app->request->post('file');  // ile binary
    $part_file = explode(".", $app->request->post('name'));
    $ExtensionFile = $part_file[(count($part_file)-1)];     // Tipo file
    $new_file = substr( $app->request->post('name'),0,(strlen($app->request->post('name'))-strlen($ExtensionFile)-1)).".pdf";

    $option = $app->request->post('option');
    
    // -------------------- for create temporaly file --------------------------
    $temp_file = tempnam(sys_get_temp_dir(), 'temp');
    $part_path = pathinfo($temp_file);
    $temp_file1 = $part_path['dirname'].'/'.$part_path['filename'].'.'.$ExtensionFile;

    $phpLog = $part_path['dirname'].'/phplog.txt'; // Trace of Debug code
        
    $fp = fopen($temp_file1, 'w');
    $fwrite = fwrite($fp, $BlobInput);
    fclose($fp);
    
    $sizeFile = filesize($temp_file1);
  
    $temp_file2 = $part_path['dirname'].'/'.$part_path['filename'].'.pdf';

    $fileLock = $part_path['dirname'].'/lock.txt';
    if (!file_exists($fileLock)) { // Si no existe, se crea
        $fpLock = fopen("$fileLock", "w");
        fclose($fpLock);
    }
    $fp = fopen($fileLock, "r+");		// Fcihero de bloqueos para que sólo haya una única ejecución
    do { // Bucle de conversión copn control de que sólo un proceso puede estar en ejecución
        
        if (flock($fp, LOCK_EX)) {  // adquirir un bloqueo exclusivo

           // Convert to PDF
           // $result = shell_exec("..\\Office_to_PDF\\OfficeToPDF.exe $temp_file1 $temp_file2");
             $status = exec("..\\Office_to_PDF\\OfficeToPDF.exe $option $temp_file1 $temp_file2", $outputCommand, $result);

            if ($result <> 0) {
               // LOG
               $a = "Resultado de conversion: ".$result."\n".var_export($outputCommand, true)." \n";
               file_put_contents($phpLog, $a, FILE_APPEND | LOCK_EX);
            }
            flock($fp, LOCK_UN);    // libera el bloqueo
            break;
        } else {
          sleep(1);
        }

    } while (true);
    fclose($fp);

    // ------------------ Operation with file result -------------------------------------------
    $document = file_get_contents($temp_file2);
    unlink($temp_file);   // delete file tmp
    unlink($temp_file1);  // delete file tmp
    unlink($temp_file2);  // delete file tmp

    $response = array();
    //capturamos los parametros recibidos y los almacxenamos como un nuevo array
    $param['name']  = $new_file;
    $param['file'] = base64_encode($document);


    if ( is_array($param) ) {
        $response["error"] = false;
        $response["message"] = "Documento convertido satisfactoriamente!";
        $response["document"] = $param;
    } else {
        $response["error"] = true;
        $response["message"] = "Error al crear auto. Por favor intenta nuevamente.";
    }
    echoResponse(201, $response);
    
    // Write LOG
    $data = array();
    $data["nameFile"] = $new_file;
    $data["sizeFile"]  = $sizeFile;
    $data["startConvert"] = $startConvert;
    $data["endConvert"] = date("Y-m-d H:i:s");
    DB::Insert("server_pdf_log", $data );
});


/* corremos la aplicación */
$app->run();

/*********************** USEFULL FUNCTIONS **************************************/

/**
 * Verificando los parametros requeridos en el metodo o endpoint
 */
function verifyRequiredParams($required_fields) {
    $error = false;
    $error_fields = "";
    $request_params = array();
    $request_params = $_REQUEST;
    // Handling PUT request params
    if ($_SERVER['REQUEST_METHOD'] == 'PUT') {
        $app = \Slim\Slim::getInstance();
        parse_str($app->request()->getBody(), $request_params);
    }
    foreach ($required_fields as $field) {
        if (!isset($request_params[$field]) || strlen(trim($request_params[$field])) <= 0) {
            $error = true;
            $error_fields .= $field . ', ';
        }
    }
 
    if ($error) {
        // Required field(s) are missing or empty
        // echo error json and stop the app
        $response = array();
        $app = \Slim\Slim::getInstance();
        $response["error"] = true;
        $response["message"] = 'Required field(s) ' . substr($error_fields, 0, -2) . ' is missing or empty';
        echoResponse(400, $response);
        
        $app->stop();
    }
}
 
 
/**
 * Mostrando la respuesta en formato json al cliente o navegador
 * @param String $status_code Http response code
 * @param Int $response Json response
 */
function echoResponse($status_code, $response) {
    $app = \Slim\Slim::getInstance();
    // Http response code
    $app->status($status_code);
 
    // setting response content type to json
    $app->contentType('application/json');
 
    echo json_encode($response);
}

/**
 * Adding an intermediate layer of authentication for one or all methods, use as needed
 * Revisa si la consulta contiene un Header "Authorization" para validar
 */
function authenticate(\Slim\Route $route) {
    // Getting request headers
    $headers = apache_request_headers();
    // LOG
    // $a = var_export($headers, true);
    // file_put_contents("C:/tmp/php/error/php.log", $a, FILE_APPEND | LOCK_EX);

    $response = array();
    $app = \Slim\Slim::getInstance();
 
    // Verifying Authorization Header
    if (isset($headers['Authorization'])) {
        //$db = new DbHandler(); //utilizar para manejar autenticacion contra base de datos
 
        // get the api key
        $token = $headers['Authorization'];
        
        // validating api key
        if (!($token == API_KEY)) { //API_KEY declarada en Config.php
            
            // api key is not present in users table
            $response["error"] = true;
            $response["message"] = "Acceso denegado. Token inválido";
            echoResponse(401, $response);
            
            $app->stop(); //Detenemos la ejecución del programa al no validar
            
        } else {
            //procede utilizar el recurso o metodo del llamado
        }
    } else {
        // api key is missing in header
        $response["error"] = true;
        $response["message"] = "Falta token de autorización";
        echoResponse(400, $response);
        
        $app->stop();
    }
}
?>

 

En ficheros adjuntos os dejo los dos proyectos (PHPRunner 10.4) para que los probéis en vuestros PC’s  y como siempre, para cualquier duda que tengáis, podéis contactar conmigo en [email protected]

En otros 2 ficheros adjuntos os dejo los proyectos convertidos a PHPRunner 10.7 y adaptado para utilizar PHP 8.1.

Blog personal para facilitar soporte gratuito a usuarios de PHPRunner