<?php
namespace App\Controller\Api;
use App\Service\OnlyOfficeDocBuilderService;
use App\Service\HtmlToDocBuilderConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Psr\Log\LoggerInterface;
/**
* @Route("/")
*/
class OnlyOfficeController extends AbstractController
{
private OnlyOfficeDocBuilderService $docBuilder;
private HtmlToDocBuilderConverter $htmlConverter;
private LoggerInterface $logger;
public function __construct(
OnlyOfficeDocBuilderService $docBuilder,
HtmlToDocBuilderConverter $htmlConverter,
LoggerInterface $logger
) {
$this->docBuilder = $docBuilder;
$this->htmlConverter = $htmlConverter;
$this->logger = $logger;
}
/**
* Convert document to given format
* @Route("/convert-to", name="api_onlyoffice_convert_to", methods={"POST"})
*/
public function convertTo(Request $request): Response
{
$data = json_decode($request->getContent(), true);
if (!isset($data['document_url'])) {
return $this->json(['error' => 'document_url is required'], Response::HTTP_BAD_REQUEST);
}
if (!isset($data['output_format'])) {
return $this->json(['error' => 'output_format is required'], Response::HTTP_BAD_REQUEST);
}
try {
$convertedFile = $this->processDocument($data['document_url'], $data['output_format'], [
'type' => 'convert',
]);
return new Response($convertedFile);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Convert document to PDF
* @Route("/convert-to-pdf", name="api_onlyoffice_convert_to_pdf", methods={"POST"})
*/
public function convertToPdf(Request $request): Response
{
$data = json_decode($request->getContent(), true);
if (!isset($data['document_url'])) {
return $this->json(['error' => 'document_url is required'], Response::HTTP_BAD_REQUEST);
}
try {
$convertedFile = $this->processDocument($data['document_url'], 'pdf', [
'type' => 'convert',
]);
return new Response($convertedFile);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Replace labels with text in document
* @Route("/replace-labels-with-text", name="api_onlyoffice_replace_labels_with_text", methods={"POST"})
*/
public function replaceLabelsWithText(Request $request): Response
{
$data = json_decode($request->getContent(), true);
if (!isset($data['document_url']) || !isset($data['labels'])) {
return $this->json(
['error' => 'document_url and labels are required'],
Response::HTTP_BAD_REQUEST
);
}
try {
$resultFile = $this->processDocument($data['document_url'], 'docx', [
'type' => 'replaceText',
'labels' => $data['labels']
]);
return new Response($resultFile);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Replace labels with HTML content in document.
* Unlike replaceLabelsWithText, this method interprets HTML content and generates
* structured document elements (paragraphs, headings, tables, lists, formatted text, etc.)
*
* Supported HTML elements:
* - Block elements: <p>, <h1>, <h2>, <h3>, <div>
* - Text formatting: <strong>, <b>, <em>, <i>, <u>
* - Lists: <ul>, <ol>, <li>
* - Tables: <table>, <tr>, <td>, <th>, <thead>, <tbody>
* - Media: <img> (with src, width, height attributes)
* - Line breaks: <br>
*
* @Route("/replace-labels-html-with-text", name="api_onlyoffice_replace_labels_html_with_text", methods={"POST"})
*/
public function replaceLabelsHtmlWithText(Request $request): Response
{
$data = json_decode($request->getContent(), true);
if (!isset($data['document_url']) || !isset($data['labels'])) {
return $this->json(
['error' => 'document_url and labels are required'],
Response::HTTP_BAD_REQUEST
);
}
try {
$processedLabels = [];
foreach ($data['labels'] as $labelKey => $labelValue) {
$processedLabels[$labelKey] = [
'content' => $labelValue,
'docbuilder' => $this->htmlConverter->htmlToDocBuilder($labelValue)
];
}
$resultFile = $this->processDocument($data['document_url'], 'docx', [
'type' => 'replaceHtml',
// 'labels' => $data['labels']
'labels' => $processedLabels,
]);
return new Response($resultFile);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Replace labels with text in document
* @Route("/replace-labels-with-image", name="api_onlyoffice_replace_labels_with_image", methods={"POST"})
*/
public function replaceLabelsWithImage(Request $request): Response
{
$data = json_decode($request->getContent(), true);
if (!isset($data['document_url']) || !isset($data['image'])) {
return $this->json(
['error' => 'document_url and image are required'],
Response::HTTP_BAD_REQUEST
);
}
try {
$resultFile = $this->processDocument($data['document_url'], 'docx', [
'type' => 'replaceImage',
'label' => $data['label'],
'image' => $data['image'],
'width' => intval($data['width'] ?? 220),
'height' => intval($data['height'] ?? 250)
]);
return new Response($resultFile);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Add watermark to document
* @Route("/add-watermark", name="api_onlyoffice_add_watermark", methods={"POST"})
*/
public function addWatermark(Request $request): Response
{
$data = json_decode($request->getContent(), true);
if (!isset($data['document_url']) || !isset($data['watermark'])) {
return $this->json(
['error' => 'document_url and watermark are required'],
Response::HTTP_BAD_REQUEST
);
}
try {
$resultFile = $this->processDocument($data['document_url'], 'docx', [
'type' => 'watermark',
'watermark' => $data['watermark'],
'watermark_type' => $data['watermark_type'] ?? 'text',
]);
return new Response($resultFile);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function processDocument(string $documentUrl, $outputFormat, array $options): string
{
$outName = 'result_' . bin2hex(random_bytes(6)) . '.' . $outputFormat;
// Script para OnlyOffice DocBuilder
$script = "builder.SetTmpFolder('DocBuilderTemp');\nbuilder.OpenFile('$documentUrl', '');\nvar doc = Api.GetDocument();\n";
switch ($options['type']) {
case 'replaceText':
$labels = $options['labels'];
foreach ($labels as $key => $value) {
$script .= "doc.SearchAndReplace({searchString: '" . addslashes($key) . "', replaceString: '" . addslashes($value) . "', matchCase: false});\n";
}
break;
case 'replaceImage':
$label = $options['label'];
$image = $options['image'];
$width = $options['width'];
$height = $options['height'];
$wEMU = intval(round($width * 3600));
$hEMU = intval(round($height * 3600));
$script .= "var found = doc.Search('" . addslashes($label) . "');\nif (found && found.length>0){ found[0].Select();\nvar p = Api.CreateParagraph();\nvar img = Api.CreateImage('" . addslashes($image) . "', $wEMU, $hEMU);\nimg.SetWrappingStyle('inline');\np.AddDrawing(img);\ndoc.InsertContent([p]);\n}\n";
break;
case 'watermark':
$watermarkType = $options['watermark_type'] ?? 'text';
if ($watermarkType === 'text') {
$texto = $options['watermark'];
$script .= "doc.InsertWatermark('$texto', true);\n";
} else {
$image = $options['watermark'];
$script .= "var sec=doc.GetFinalSection();\nvar wTw=sec.GetPageWidth();\nvar hTw=sec.GetPageHeight();\nvar EMU_PER_TWIP=635;\nvar wEMU=wTw*EMU_PER_TWIP;\nvar hEMU=hTw*EMU_PER_TWIP;\ndoc.InsertWatermark('WATERMARK',false);\nvar ws=doc.GetWatermarkSettings();\nws.SetType('image');\nws.SetImageURL('" . addslashes($image) . "');\nws.SetImageSize(wEMU,hEMU);\nws.SetDirection('horizontal');\nws.SetOpacity(100);\ndoc.SetWatermarkSettings(ws);\n";
}
break;
case 'replaceHtml':
$labels = $options['labels'];
foreach ($labels as $key => $value) {
$content = $value['content'];
if (strip_tags($content) === $content) {
$script .= "doc.SearchAndReplace({searchString: '" . addslashes($key) . "', replaceString: '" . addslashes($content) . "', matchCase: false});\n";
} else {
$script .= "var found = doc.Search('" . addslashes($key) . "');\n";
$script .= "if (found && found.length > 0) {\n";
$script .= " for (var fi = found.length - 1; fi >= 0; fi--) {\n";
$script .= " var range = found[fi];\n";
$script .= " var _content = [];\n";
$script .= " var _inlinePara = Api.CreateParagraph();\n";
$script .= " var _inlineUsed = false;\n";
$script .= " var oPara = {\n";
$script .= " AddElement: function(el) { _inlineUsed = true; _inlinePara.AddElement(el); },\n";
$script .= " GetParentTableCell: function() {\n";
$script .= " return {\n";
$script .= " GetContent: function() {\n";
$script .= " return { Push: function(el) { _content.push(el); } };\n";
$script .= " }\n";
$script .= " };\n";
$script .= " }\n";
$script .= " };\n";
$script .= " " . $value['docbuilder'] . "\n";
$script .= " if (_inlineUsed) { _content.unshift(_inlinePara); }\n";
$script .= " range.Select();\n";
$script .= " if (_content.length > 0) { doc.InsertContent(_content); }\n";
$script .= " }\n";
$script .= "}\n";
}
}
break;
default:
break;
}
// Cerrar y guardar el documento
$script .= "builder.SaveFile('$outputFormat', '$outName');\nbuilder.CloseFile();";
$this->logger->debug('Generated DocBuilder script', [
'type' => $options['type'],
'script' => $script
]);
try {
// Build the document and upload to Google Cloud Storage
$urlScript = $this->docBuilder->construirSubirDocumento($script);
$doc = $this->docBuilder->enviarDocBuilder($urlScript, $outName, $outputFormat);
return $doc['urls'][$outName];
} catch (\Exception $e) {
$this->logger->error('Error al convertir documento', [
'error' => $e->getMessage(),
'documentUrl' => $documentUrl,
'outputFormat' => $outputFormat,
'trace' => $e->getTraceAsString()
]);
throw new \RuntimeException('Failed to convert document: ' . $e->getMessage());
}
}
}