src/Controller/Api/OnlyOfficeController.php line 63

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Api;
  3. use App\Service\OnlyOfficeDocBuilderService;
  4. use App\Service\HtmlToDocBuilderConverter;
  5. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\Routing\Annotation\Route;
  9. use Psr\Log\LoggerInterface;
  10. /**
  11. * @Route("/")
  12. */
  13. class OnlyOfficeController extends AbstractController
  14. {
  15. private OnlyOfficeDocBuilderService $docBuilder;
  16. private HtmlToDocBuilderConverter $htmlConverter;
  17. private LoggerInterface $logger;
  18. public function __construct(
  19. OnlyOfficeDocBuilderService $docBuilder,
  20. HtmlToDocBuilderConverter $htmlConverter,
  21. LoggerInterface $logger
  22. ) {
  23. $this->docBuilder = $docBuilder;
  24. $this->htmlConverter = $htmlConverter;
  25. $this->logger = $logger;
  26. }
  27. /**
  28. * Convert document to given format
  29. * @Route("/convert-to", name="api_onlyoffice_convert_to", methods={"POST"})
  30. */
  31. public function convertTo(Request $request): Response
  32. {
  33. $data = json_decode($request->getContent(), true);
  34. if (!isset($data['document_url'])) {
  35. return $this->json(['error' => 'document_url is required'], Response::HTTP_BAD_REQUEST);
  36. }
  37. if (!isset($data['output_format'])) {
  38. return $this->json(['error' => 'output_format is required'], Response::HTTP_BAD_REQUEST);
  39. }
  40. try {
  41. $convertedFile = $this->processDocument($data['document_url'], $data['output_format'], [
  42. 'type' => 'convert',
  43. ]);
  44. return new Response($convertedFile);
  45. } catch (\Exception $e) {
  46. return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  47. }
  48. }
  49. /**
  50. * Convert document to PDF
  51. * @Route("/convert-to-pdf", name="api_onlyoffice_convert_to_pdf", methods={"POST"})
  52. */
  53. public function convertToPdf(Request $request): Response
  54. {
  55. $data = json_decode($request->getContent(), true);
  56. if (!isset($data['document_url'])) {
  57. return $this->json(['error' => 'document_url is required'], Response::HTTP_BAD_REQUEST);
  58. }
  59. try {
  60. $convertedFile = $this->processDocument($data['document_url'], 'pdf', [
  61. 'type' => 'convert',
  62. ]);
  63. return new Response($convertedFile);
  64. } catch (\Exception $e) {
  65. return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  66. }
  67. }
  68. /**
  69. * Replace labels with text in document
  70. * @Route("/replace-labels-with-text", name="api_onlyoffice_replace_labels_with_text", methods={"POST"})
  71. */
  72. public function replaceLabelsWithText(Request $request): Response
  73. {
  74. $data = json_decode($request->getContent(), true);
  75. if (!isset($data['document_url']) || !isset($data['labels'])) {
  76. return $this->json(
  77. ['error' => 'document_url and labels are required'],
  78. Response::HTTP_BAD_REQUEST
  79. );
  80. }
  81. try {
  82. $resultFile = $this->processDocument($data['document_url'], 'docx', [
  83. 'type' => 'replaceText',
  84. 'labels' => $data['labels']
  85. ]);
  86. return new Response($resultFile);
  87. } catch (\Exception $e) {
  88. return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  89. }
  90. }
  91. /**
  92. * Replace labels with HTML content in document.
  93. * Unlike replaceLabelsWithText, this method interprets HTML content and generates
  94. * structured document elements (paragraphs, headings, tables, lists, formatted text, etc.)
  95. *
  96. * Supported HTML elements:
  97. * - Block elements: <p>, <h1>, <h2>, <h3>, <div>
  98. * - Text formatting: <strong>, <b>, <em>, <i>, <u>
  99. * - Lists: <ul>, <ol>, <li>
  100. * - Tables: <table>, <tr>, <td>, <th>, <thead>, <tbody>
  101. * - Media: <img> (with src, width, height attributes)
  102. * - Line breaks: <br>
  103. *
  104. * @Route("/replace-labels-html-with-text", name="api_onlyoffice_replace_labels_html_with_text", methods={"POST"})
  105. */
  106. public function replaceLabelsHtmlWithText(Request $request): Response
  107. {
  108. $data = json_decode($request->getContent(), true);
  109. if (!isset($data['document_url']) || !isset($data['labels'])) {
  110. return $this->json(
  111. ['error' => 'document_url and labels are required'],
  112. Response::HTTP_BAD_REQUEST
  113. );
  114. }
  115. try {
  116. $processedLabels = [];
  117. foreach ($data['labels'] as $labelKey => $labelValue) {
  118. $processedLabels[$labelKey] = [
  119. 'content' => $labelValue,
  120. 'docbuilder' => $this->htmlConverter->htmlToDocBuilder($labelValue)
  121. ];
  122. }
  123. $resultFile = $this->processDocument($data['document_url'], 'docx', [
  124. 'type' => 'replaceHtml',
  125. // 'labels' => $data['labels']
  126. 'labels' => $processedLabels,
  127. ]);
  128. return new Response($resultFile);
  129. } catch (\Exception $e) {
  130. return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  131. }
  132. }
  133. /**
  134. * Replace labels with text in document
  135. * @Route("/replace-labels-with-image", name="api_onlyoffice_replace_labels_with_image", methods={"POST"})
  136. */
  137. public function replaceLabelsWithImage(Request $request): Response
  138. {
  139. $data = json_decode($request->getContent(), true);
  140. if (!isset($data['document_url']) || !isset($data['image'])) {
  141. return $this->json(
  142. ['error' => 'document_url and image are required'],
  143. Response::HTTP_BAD_REQUEST
  144. );
  145. }
  146. try {
  147. $resultFile = $this->processDocument($data['document_url'], 'docx', [
  148. 'type' => 'replaceImage',
  149. 'label' => $data['label'],
  150. 'image' => $data['image'],
  151. 'width' => intval($data['width'] ?? 220),
  152. 'height' => intval($data['height'] ?? 250)
  153. ]);
  154. return new Response($resultFile);
  155. } catch (\Exception $e) {
  156. return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  157. }
  158. }
  159. /**
  160. * Add watermark to document
  161. * @Route("/add-watermark", name="api_onlyoffice_add_watermark", methods={"POST"})
  162. */
  163. public function addWatermark(Request $request): Response
  164. {
  165. $data = json_decode($request->getContent(), true);
  166. if (!isset($data['document_url']) || !isset($data['watermark'])) {
  167. return $this->json(
  168. ['error' => 'document_url and watermark are required'],
  169. Response::HTTP_BAD_REQUEST
  170. );
  171. }
  172. try {
  173. $resultFile = $this->processDocument($data['document_url'], 'docx', [
  174. 'type' => 'watermark',
  175. 'watermark' => $data['watermark'],
  176. 'watermark_type' => $data['watermark_type'] ?? 'text',
  177. ]);
  178. return new Response($resultFile);
  179. } catch (\Exception $e) {
  180. return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
  181. }
  182. }
  183. private function processDocument(string $documentUrl, $outputFormat, array $options): string
  184. {
  185. $outName = 'result_' . bin2hex(random_bytes(6)) . '.' . $outputFormat;
  186. // Script para OnlyOffice DocBuilder
  187. $script = "builder.SetTmpFolder('DocBuilderTemp');\nbuilder.OpenFile('$documentUrl', '');\nvar doc = Api.GetDocument();\n";
  188. switch ($options['type']) {
  189. case 'replaceText':
  190. $labels = $options['labels'];
  191. foreach ($labels as $key => $value) {
  192. $script .= "doc.SearchAndReplace({searchString: '" . addslashes($key) . "', replaceString: '" . addslashes($value) . "', matchCase: false});\n";
  193. }
  194. break;
  195. case 'replaceImage':
  196. $label = $options['label'];
  197. $image = $options['image'];
  198. $width = $options['width'];
  199. $height = $options['height'];
  200. $wEMU = intval(round($width * 3600));
  201. $hEMU = intval(round($height * 3600));
  202. $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";
  203. break;
  204. case 'watermark':
  205. $watermarkType = $options['watermark_type'] ?? 'text';
  206. if ($watermarkType === 'text') {
  207. $texto = $options['watermark'];
  208. $script .= "doc.InsertWatermark('$texto', true);\n";
  209. } else {
  210. $image = $options['watermark'];
  211. $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";
  212. }
  213. break;
  214. case 'replaceHtml':
  215. $labels = $options['labels'];
  216. foreach ($labels as $key => $value) {
  217. $content = $value['content'];
  218. if (strip_tags($content) === $content) {
  219. $script .= "doc.SearchAndReplace({searchString: '" . addslashes($key) . "', replaceString: '" . addslashes($content) . "', matchCase: false});\n";
  220. } else {
  221. $script .= "var found = doc.Search('" . addslashes($key) . "');\n";
  222. $script .= "if (found && found.length > 0) {\n";
  223. $script .= " for (var fi = found.length - 1; fi >= 0; fi--) {\n";
  224. $script .= " var range = found[fi];\n";
  225. $script .= " var _content = [];\n";
  226. $script .= " var _inlinePara = Api.CreateParagraph();\n";
  227. $script .= " var _inlineUsed = false;\n";
  228. $script .= " var oPara = {\n";
  229. $script .= " AddElement: function(el) { _inlineUsed = true; _inlinePara.AddElement(el); },\n";
  230. $script .= " GetParentTableCell: function() {\n";
  231. $script .= " return {\n";
  232. $script .= " GetContent: function() {\n";
  233. $script .= " return { Push: function(el) { _content.push(el); } };\n";
  234. $script .= " }\n";
  235. $script .= " };\n";
  236. $script .= " }\n";
  237. $script .= " };\n";
  238. $script .= " " . $value['docbuilder'] . "\n";
  239. $script .= " if (_inlineUsed) { _content.unshift(_inlinePara); }\n";
  240. $script .= " range.Select();\n";
  241. $script .= " if (_content.length > 0) { doc.InsertContent(_content); }\n";
  242. $script .= " }\n";
  243. $script .= "}\n";
  244. }
  245. }
  246. break;
  247. default:
  248. break;
  249. }
  250. // Cerrar y guardar el documento
  251. $script .= "builder.SaveFile('$outputFormat', '$outName');\nbuilder.CloseFile();";
  252. $this->logger->debug('Generated DocBuilder script', [
  253. 'type' => $options['type'],
  254. 'script' => $script
  255. ]);
  256. try {
  257. // Build the document and upload to Google Cloud Storage
  258. $urlScript = $this->docBuilder->construirSubirDocumento($script);
  259. $doc = $this->docBuilder->enviarDocBuilder($urlScript, $outName, $outputFormat);
  260. return $doc['urls'][$outName];
  261. } catch (\Exception $e) {
  262. $this->logger->error('Error al convertir documento', [
  263. 'error' => $e->getMessage(),
  264. 'documentUrl' => $documentUrl,
  265. 'outputFormat' => $outputFormat,
  266. 'trace' => $e->getTraceAsString()
  267. ]);
  268. throw new \RuntimeException('Failed to convert document: ' . $e->getMessage());
  269. }
  270. }
  271. }