PageRenderTime 52ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/civicrm/custom/php/CRM/Utils/PDF/Utils.php

https://github.com/nysenate/Bluebird-CRM
PHP | 384 lines | 232 code | 45 blank | 107 comment | 23 complexity | e2f46d0c9aa95d4f0c09349dd79f8c3c MD5 | raw file
Possible License(s): JSON, BSD-3-Clause, MPL-2.0-no-copyleft-exception, AGPL-1.0, GPL-2.0, AGPL-3.0, Apache-2.0, MIT, GPL-3.0, CC-BY-4.0, LGPL-2.1, BSD-2-Clause, LGPL-3.0
  1. <?php
  2. /*
  3. +--------------------------------------------------------------------+
  4. | Copyright CiviCRM LLC. All rights reserved. |
  5. | |
  6. | This work is published under the GNU AGPLv3 license with some |
  7. | permitted exceptions and without any warranty. For full license |
  8. | and copyright information, see https://civicrm.org/licensing |
  9. +--------------------------------------------------------------------+
  10. */
  11. use Dompdf\Dompdf;
  12. use Dompdf\Options;
  13. /**
  14. *
  15. * @package CRM
  16. * @copyright CiviCRM LLC https://civicrm.org/licensing
  17. */
  18. class CRM_Utils_PDF_Utils {
  19. //NYSS use a 4MiB chunk size
  20. const CHUNK_SIZE = 4194304;
  21. // No need to strip extra whitespace from the HTML, since the incoming
  22. // HTML should now be optimized. Set this to TRUE to force this potentially
  23. // redundant search-and-replace.
  24. const HTML_STRIP_SPACE = false;
  25. /**
  26. * @param array $text
  27. * List of HTML snippets.
  28. * @param string $fileName
  29. * The logical filename to display.
  30. * Ex: "HelloWorld.pdf".
  31. * @param bool $output
  32. * FALSE to display PDF. TRUE to return as string.
  33. * @param null $pdfFormat
  34. * Unclear. Possibly PdfFormat or formValues.
  35. *
  36. * @return string|void
  37. */
  38. public static function html2pdf($text, $fileName = 'civicrm.pdf', $output = FALSE, $pdfFormat = NULL) {
  39. if (is_array($text)) {
  40. $pages = &$text;
  41. }
  42. else {
  43. $pages = [$text];
  44. }
  45. // Get PDF Page Format
  46. $format = CRM_Core_BAO_PdfFormat::getDefaultValues();
  47. if (is_array($pdfFormat)) {
  48. // PDF Page Format parameters passed in
  49. $format = array_merge($format, $pdfFormat);
  50. }
  51. elseif (!empty($pdfFormat)) {
  52. // PDF Page Format ID passed in
  53. $format = CRM_Core_BAO_PdfFormat::getById($pdfFormat);
  54. }
  55. $paperSize = CRM_Core_BAO_PaperSize::getByName($format['paper_size']);
  56. $paper_width = self::convertMetric($paperSize['width'], $paperSize['metric'], 'pt');
  57. $paper_height = self::convertMetric($paperSize['height'], $paperSize['metric'], 'pt');
  58. // dompdf requires dimensions in points
  59. $paper_size = [0, 0, $paper_width, $paper_height];
  60. $orientation = CRM_Core_BAO_PdfFormat::getValue('orientation', $format);
  61. $metric = CRM_Core_BAO_PdfFormat::getValue('metric', $format);
  62. $t = CRM_Core_BAO_PdfFormat::getValue('margin_top', $format);
  63. $r = CRM_Core_BAO_PdfFormat::getValue('margin_right', $format);
  64. $b = CRM_Core_BAO_PdfFormat::getValue('margin_bottom', $format);
  65. $l = CRM_Core_BAO_PdfFormat::getValue('margin_left', $format);
  66. $margins = [$metric, $t, $r, $b, $l];
  67. // Add a special region for the HTML header of PDF files:
  68. $pdfHeaderRegion = CRM_Core_Region::instance('export-document-header', FALSE);
  69. $htmlHeader = ($pdfHeaderRegion) ? $pdfHeaderRegion->render('', FALSE) : '';
  70. $html = "
  71. <html>
  72. <head>
  73. <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>
  74. <style>@page { margin: {$t}{$metric} {$r}{$metric} {$b}{$metric} {$l}{$metric}; }</style>
  75. <style type=\"text/css\">@import url(" . CRM_Core_Config::singleton()->userFrameworkResourceURL . "css/print.css);</style>
  76. {$htmlHeader}
  77. </head>
  78. <body>
  79. <div id=\"crm-container\">\n";
  80. //NYSS
  81. $html_tmpfile = sys_get_temp_dir().DIRECTORY_SEPARATOR.uniqid('bbreport-', true).'.html';
  82. $fp = fopen($html_tmpfile, "w");
  83. fwrite($fp, $html);
  84. // Strip <html>, <header>, and <body> tags from each page
  85. //NYSS split into head/tail to account for chunk processing; add script tag
  86. $headerElems = array(
  87. '@<head[^>]*?>.*?</head>@siu',
  88. '@<script[^>]*?>.*?</script>@siu',
  89. '@<body>@siu',
  90. '@</body>@siu',
  91. '@<html[^>]*?>@siu',
  92. '@</html>@siu',
  93. '@<!DOCTYPE[^>]*?>@siu',
  94. );
  95. $tailElems = array(
  96. '@</body>@siu',
  97. '@</html>@siu',
  98. );
  99. for ($i = 0; $i < count($pages); $i++) {
  100. if ($i > 0) {
  101. fwrite($fp, "\n<div style=\"page-break-after: always\"></div>\n");
  102. }
  103. $cur_page =& $pages[$i];
  104. $cur_page_len = strlen($cur_page);
  105. $startpos = 0;
  106. while ($startpos < $cur_page_len) {
  107. $cur_chunk = substr($cur_page, $startpos, self::CHUNK_SIZE);
  108. // Some optimized search-and-replace:
  109. // First chunk should have <!DOCTYPE>, <html>, <head>, </head>, <body>
  110. // Last chunk should have </body> and </html>
  111. if ($startpos == 0) {
  112. $cur_chunk = preg_replace($headerElems, '', $cur_chunk);
  113. }
  114. // The </body> and </html> should be in the last chunk, but could
  115. // have been split between the last and second-to-last chunks. That's
  116. // why I allow for 64 characters extra.
  117. if ($startpos > $cur_page_len - self::CHUNK_SIZE - 64) {
  118. $cur_chunk = preg_replace($tailElems, '', $cur_chunk);
  119. }
  120. // Now eliminate extra spaces, and add newlines after end-tags.
  121. if (self::HTML_STRIP_SPACE === true) {
  122. $cur_chunk = preg_replace(array('/>\s+/', '/\s+</', '#</[^>]+>#'),
  123. array('>', '<', "$0\n"), $cur_chunk);
  124. }
  125. fwrite($fp, $cur_chunk);
  126. $startpos += self::CHUNK_SIZE;
  127. }
  128. }
  129. // Glue the pages together
  130. $html .= implode("\n<div style=\"page-break-after: always\"></div>\n", $pages);
  131. $html .= "
  132. </div>
  133. </body>
  134. </html>";
  135. fwrite($fp, $html);
  136. fclose($fp);
  137. $pages = NULL; //force garbage collection on the original HTML? Try it.
  138. if (CRM_Core_Config::singleton()->wkhtmltopdfPath) {
  139. $rc = self::_html2pdf_wkhtmltopdf($paper_size, $orientation, $margins, $html_tmpfile, $output, $fileName, TRUE);
  140. }
  141. else {
  142. $rc = self::_html2pdf_dompdf($paper_size, $orientation, $html, $output, $fileName);
  143. }
  144. unlink($html_tmpfile);
  145. return $rc;
  146. }
  147. /**
  148. * Convert html to tcpdf.
  149. *
  150. * @deprecated
  151. * @param $paper_size
  152. * @param $orientation
  153. * @param $margins
  154. * @param $html
  155. * @param $output
  156. * @param $fileName
  157. * @param $stationery_path
  158. */
  159. public static function _html2pdf_tcpdf($paper_size, $orientation, $margins, $html, $output, $fileName, $stationery_path) {
  160. CRM_Core_Error::deprecatedFunctionWarning('CRM_Utils_PDF::_html2pdf_dompdf');
  161. return self::_html2pdf_dompdf($paper_size, $orientation, $margins, $html, $output, $fileName);
  162. // Documentation on the TCPDF library can be found at: http://www.tcpdf.org
  163. // This function also uses the FPDI library documented at: http://www.setasign.com/products/fpdi/about/
  164. // Syntax borrowed from https://github.com/jake-mw/CDNTaxReceipts/blob/master/cdntaxreceipts.functions.inc
  165. require_once 'tcpdf/tcpdf.php';
  166. // This library is only in the 'packages' area as of version 4.5
  167. require_once 'FPDI/fpdi.php';
  168. $paper_size_arr = [$paper_size[2], $paper_size[3]];
  169. $pdf = new TCPDF($orientation, 'pt', $paper_size_arr);
  170. $pdf->Open();
  171. if (is_readable($stationery_path)) {
  172. $pdf->SetStationery($stationery_path);
  173. }
  174. $pdf->SetAuthor('');
  175. $pdf->SetKeywords('CiviCRM.org');
  176. $pdf->setPageUnit($margins[0]);
  177. $pdf->SetMargins($margins[4], $margins[1], $margins[2], TRUE);
  178. $pdf->setJPEGQuality('100');
  179. $pdf->SetAutoPageBreak(TRUE, $margins[3]);
  180. $pdf->AddPage();
  181. $ln = TRUE;
  182. $fill = FALSE;
  183. $reset_parm = FALSE;
  184. $cell = FALSE;
  185. $align = '';
  186. // output the HTML content
  187. $pdf->writeHTML($html, $ln, $fill, $reset_parm, $cell, $align);
  188. // reset pointer to the last page
  189. $pdf->lastPage();
  190. // close and output the PDF
  191. $pdf->Close();
  192. $pdf_file = 'CiviLetter' . '.pdf';
  193. $pdf->Output($pdf_file, 'D');
  194. CRM_Utils_System::civiExit();
  195. }
  196. /**
  197. * @param $paper_size
  198. * @param $orientation
  199. * @param $html
  200. * @param $output
  201. * @param string $fileName
  202. *
  203. * @return string
  204. */
  205. public static function _html2pdf_dompdf($paper_size, $orientation, $html, $output, $fileName) {
  206. $options = self::getDompdfOptions();
  207. $dompdf = new DOMPDF($options);
  208. $dompdf->set_paper($paper_size, $orientation);
  209. $dompdf->load_html($html);
  210. $dompdf->render();
  211. if ($output) {
  212. return $dompdf->output();
  213. }
  214. // CRM-19183 remove .pdf extension from filename
  215. $fileName = basename($fileName, ".pdf");
  216. if (CIVICRM_UF === 'UnitTests' && headers_sent()) {
  217. // Streaming content will 'die' in unit tests unless ob_start()
  218. // has been called.
  219. throw new CRM_Core_Exception_PrematureExitException('_html2pdf_dompdf called', [
  220. 'html' => $html,
  221. 'fileName' => $fileName,
  222. ]);
  223. }
  224. $dompdf->stream($fileName);
  225. }
  226. /**
  227. * @param $paper_size
  228. * @param $orientation
  229. * @param $margins
  230. * @param $html
  231. * @param $output
  232. * @param string $fileName
  233. */
  234. //NYSS support passing filename
  235. public static function _html2pdf_wkhtmltopdf($paper_size, $orientation, $margins, $html, $output, $fileName, $fileInput = FALSE) {
  236. require_once 'packages/snappy/src/autoload.php';
  237. $config = CRM_Core_Config::singleton();
  238. $snappy = new Knp\Snappy\Pdf($config->wkhtmltopdfPath);
  239. $snappy->setOption("page-width", $paper_size[2] . "pt");
  240. $snappy->setOption("page-height", $paper_size[3] . "pt");
  241. $snappy->setOption("orientation", $orientation);
  242. $snappy->setOption("margin-top", $margins[1] . $margins[0]);
  243. $snappy->setOption("margin-right", $margins[2] . $margins[0]);
  244. $snappy->setOption("margin-bottom", $margins[3] . $margins[0]);
  245. $snappy->setOption("margin-left", $margins[4] . $margins[0]);
  246. //NYSS support passing filename instead of HTML
  247. if ($fileInput) {
  248. $pdf = $snappy->getOutput($html);
  249. }
  250. else {
  251. $pdf = $snappy->getOutputFromHtml($html);
  252. }
  253. if ($output) {
  254. return $pdf;
  255. }
  256. else {
  257. CRM_Utils_System::setHttpHeader('Content-Type', 'application/pdf');
  258. CRM_Utils_System::setHttpHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"');
  259. echo $pdf;
  260. }
  261. }
  262. /**
  263. * convert value from one metric to another.
  264. *
  265. * @param $value
  266. * @param $from
  267. * @param $to
  268. * @param null $precision
  269. *
  270. * @return float|int
  271. */
  272. public static function convertMetric($value, $from, $to, $precision = NULL) {
  273. switch ($from . $to) {
  274. case 'incm':
  275. $value *= 2.54;
  276. break;
  277. case 'inmm':
  278. $value *= 25.4;
  279. break;
  280. case 'inpt':
  281. $value *= 72;
  282. break;
  283. case 'cmin':
  284. $value /= 2.54;
  285. break;
  286. case 'cmmm':
  287. $value *= 10;
  288. break;
  289. case 'cmpt':
  290. $value *= 72 / 2.54;
  291. break;
  292. case 'mmin':
  293. $value /= 25.4;
  294. break;
  295. case 'mmcm':
  296. $value /= 10;
  297. break;
  298. case 'mmpt':
  299. $value *= 72 / 25.4;
  300. break;
  301. case 'ptin':
  302. $value /= 72;
  303. break;
  304. case 'ptcm':
  305. $value *= 2.54 / 72;
  306. break;
  307. case 'ptmm':
  308. $value *= 25.4 / 72;
  309. break;
  310. }
  311. if (!is_null($precision)) {
  312. $value = round($value, $precision);
  313. }
  314. return $value;
  315. }
  316. /**
  317. * Allow setting some dompdf options.
  318. *
  319. * We don't support all the available dompdf options.
  320. *
  321. * @return \Dompdf\Options
  322. */
  323. private static function getDompdfOptions(): Options {
  324. $options = new Options();
  325. $settings = [
  326. // CRM-12165 - Remote file support required for image handling so default to TRUE
  327. 'enable_remote' => \Civi::settings()->get('dompdf_enable_remote') ?? TRUE,
  328. ];
  329. // only set these ones if a setting exists for them
  330. foreach (['font_dir', 'chroot', 'log_output_file'] as $setting) {
  331. $value = \Civi::settings()->get("dompdf_$setting");
  332. if (isset($value)) {
  333. $settings[$setting] = $value;
  334. }
  335. }
  336. $options->set($settings);
  337. return $options;
  338. }
  339. }