1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 * 8 */ 9namespace Piwik\ReportRenderer; 10 11use Piwik\Common; 12use Piwik\Filesystem; 13use Piwik\NumberFormatter; 14use Piwik\Piwik; 15use Piwik\Plugins\API\API; 16use Piwik\Plugins\CoreAdminHome\CustomLogo; 17use Piwik\ReportRenderer; 18use Piwik\TCPDF; 19 20/** 21 * @see libs/tcpdf 22 */ 23require_once PIWIK_INCLUDE_PATH . '/plugins/ScheduledReports/config/tcpdf_config.php'; 24 25/** 26 * PDF report renderer 27 */ 28class Pdf extends ReportRenderer 29{ 30 const IMAGE_GRAPH_WIDTH_LANDSCAPE = 1050; 31 const IMAGE_GRAPH_WIDTH_PORTRAIT = 760; 32 const IMAGE_GRAPH_HEIGHT = 220; 33 34 const LANDSCAPE = 'L'; 35 const PORTRAIT = 'P'; 36 37 const MAX_ROW_COUNT = 28; 38 const TABLE_HEADER_ROW_COUNT = 6; 39 const NO_DATA_ROW_COUNT = 6; 40 const MAX_GRAPH_REPORTS = 3; 41 const MAX_2COL_TABLE_REPORTS = 2; 42 43 const PDF_CONTENT_TYPE = 'pdf'; 44 45 private $reportFontStyle = ''; 46 private $reportSimpleFontSize = 9; 47 private $reportHeaderFontSize = 16; 48 private $cellHeight = 6; 49 private $bottomMargin = 17; 50 private $reportWidthPortrait = 195; 51 private $reportWidthLandscape = 270; 52 private $minWidthLabelCell = 100; 53 private $maxColumnCountPortraitOrientation = 6; 54 private $logoWidth = 16; 55 private $logoHeight = 16; 56 private $totalWidth; 57 private $cellWidth; 58 private $labelCellWidth; 59 private $truncateAfter = 55; 60 private $leftSpacesBeforeLogo = 7; 61 private $logoImagePosition = array(10, 40); 62 private $headerTextColor; 63 private $reportTextColor; 64 private $tableHeaderBackgroundColor; 65 private $tableHeaderTextColor; 66 private $tableCellBorderColor; 67 private $tableBackgroundColor; 68 private $rowTopBottomBorder = array(231, 231, 231); 69 private $reportMetadata; 70 private $displayGraph; 71 private $evolutionGraph; 72 private $displayTable; 73 private $segment; 74 private $reportColumns; 75 private $reportRowsMetadata; 76 private $currentPage = 0; 77 private $reportFont = ReportRenderer::DEFAULT_REPORT_FONT_FAMILY; 78 private $TCPDF; 79 private $orientation = self::PORTRAIT; 80 81 public function __construct() 82 { 83 $this->TCPDF = new TCPDF(); 84 $this->headerTextColor = preg_split("/,/", ReportRenderer::REPORT_TITLE_TEXT_COLOR); 85 $this->reportTextColor = preg_split("/,/", ReportRenderer::REPORT_TEXT_COLOR); 86 $this->tableHeaderBackgroundColor = preg_split("/,/", ReportRenderer::TABLE_HEADER_BG_COLOR); 87 $this->tableHeaderTextColor = preg_split("/,/", ReportRenderer::TABLE_HEADER_TEXT_COLOR); 88 $this->tableCellBorderColor = preg_split("/,/", ReportRenderer::TABLE_CELL_BORDER_COLOR); 89 $this->tableBackgroundColor = preg_split("/,/", ReportRenderer::TABLE_BG_COLOR); 90 } 91 92 public function setLocale($locale) 93 { 94 // WARNING 95 // To make Piwik release smaller, we're deleting some fonts from the Piwik build package. 96 // If you change this code below, make sure that the fonts are NOT deleted from the Piwik package: 97 // https://github.com/piwik/piwik-package/blob/master/scripts/build-package.sh 98 switch ($locale) { 99 case 'bn': 100 case 'hi': 101 $reportFont = 'freesans'; 102 break; 103 104 case 'zh-tw': 105 $reportFont = 'msungstdlight'; 106 break; 107 108 case 'ja': 109 $reportFont = 'kozgopromedium'; 110 break; 111 112 case 'zh-cn': 113 $reportFont = 'stsongstdlight'; 114 break; 115 116 case 'ko': 117 $reportFont = 'hysmyeongjostdmedium'; 118 break; 119 120 case 'ar': 121 $reportFont = 'aealarabiya'; 122 break; 123 124 case 'am': 125 case 'ta': 126 case 'th': 127 $reportFont = 'freeserif'; 128 break; 129 130 case 'te': 131 // not working with bundled fonts 132 case 'en': 133 default: 134 $reportFont = ReportRenderer::DEFAULT_REPORT_FONT_FAMILY; 135 break; 136 } 137 // WARNING: Did you read the warning above? 138 139 $this->reportFont = $reportFont; 140 } 141 142 public function sendToDisk($filename) 143 { 144 $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE); 145 $outputFilename = ReportRenderer::getOutputPath($filename); 146 147 $this->TCPDF->Output($outputFilename, 'F'); 148 149 return $outputFilename; 150 } 151 152 public function sendToBrowserDownload($filename) 153 { 154 $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE); 155 $this->TCPDF->Output($filename, 'D'); 156 } 157 158 public function sendToBrowserInline($filename) 159 { 160 $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE); 161 $this->TCPDF->Output($filename, 'I'); 162 } 163 164 public function getRenderedReport() 165 { 166 return $this->TCPDF->Output(null, 'S'); 167 } 168 169 public function renderFrontPage($reportTitle, $prettyDate, $description, $reportMetadata, $segment) 170 { 171 $reportTitle = $this->formatText($reportTitle); 172 $dateRange = $this->formatText(Piwik::translate('General_DateRange') . " " . $prettyDate); 173 174 // footer 175 $this->TCPDF->SetFooterFont(array($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize)); 176 $this->TCPDF->SetFooterContent($reportTitle . " | " . $dateRange . " | "); 177 178 // add first page 179 $this->TCPDF->setPrintHeader(false); 180 $this->TCPDF->AddPage(self::PORTRAIT); 181 $this->TCPDF->AddFont($this->reportFont, '', '', false); 182 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize); 183 $this->TCPDF->Bookmark(Piwik::translate('ScheduledReports_FrontPage')); 184 185 // logo 186 $customLogo = new CustomLogo(); 187 $this->TCPDF->Image($customLogo->getLogoUrl(true), $this->logoImagePosition[0], $this->logoImagePosition[1], 180 / $factor = 2, 0, $type = '', $link = '', $align = '', $resize = false, $dpi = 300); 188 $this->TCPDF->Ln(8); 189 190 // report title 191 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize + 5); 192 $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]); 193 $this->TCPDF->Cell(40, 210, $reportTitle); 194 $this->TCPDF->Ln(8 * 4); 195 196 // date and period 197 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize); 198 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); 199 $this->TCPDF->Cell(40, 210, $dateRange); 200 $this->TCPDF->Ln(8 * 20); 201 202 // description 203 $this->TCPDF->Write(1, $this->formatText($description)); 204 205 // segment 206 if ($segment != null) { 207 $this->TCPDF->Ln(); 208 $this->TCPDF->Ln(); 209 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize - 2); 210 $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]); 211 $this->TCPDF->Write(1, $this->formatText(Piwik::translate('ScheduledReports_CustomVisitorSegment') . ' ' . $segment['name'])); 212 } 213 214 $this->TCPDF->Ln(8); 215 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize); 216 $this->TCPDF->Ln(); 217 } 218 219 /** 220 * Generate a header of page. 221 */ 222 private function paintReportHeader() 223 { 224 $isAggregateReport = !empty($this->reportMetadata['dimension']); 225 226 // Graph-only report 227 static $graphOnlyReportCount = 0; 228 $graphOnlyReport = $isAggregateReport && $this->displayGraph && !$this->displayTable; 229 230 // Table-only report 231 $tableOnlyReport = $isAggregateReport 232 && !$this->displayGraph 233 && $this->displayTable; 234 235 $columnCount = count($this->reportColumns); 236 237 // Table-only 2-column report 238 static $tableOnly2ColumnReportCount = 0; 239 $tableOnly2ColumnReport = $tableOnlyReport 240 && $columnCount == 2; 241 242 // Table-only report with more than 2 columns 243 static $tableOnlyManyColumnReportRowCount = 0; 244 $tableOnlyManyColumnReport = $tableOnlyReport 245 && $columnCount > 3; 246 247 $reportHasData = $this->reportHasData(); 248 249 $rowCount = $reportHasData ? $this->report->getRowsCount() + self::TABLE_HEADER_ROW_COUNT : self::NO_DATA_ROW_COUNT; 250 251 // Only a page break before if the current report has some data 252 if ($reportHasData && 253 // and 254 ( 255 // it is the first report 256 $this->currentPage == 0 257 // or, it is a graph-only report and it is the first of a series of self::MAX_GRAPH_REPORTS 258 || ($graphOnlyReport && $graphOnlyReportCount == 0) 259 // or, it is a table-only 2-column report and it is the first of a series of self::MAX_2COL_TABLE_REPORTS 260 || ($tableOnly2ColumnReport && $tableOnly2ColumnReportCount == 0) 261 // or it is a table-only report with more than 2 columns and it is the first of its series or there isn't enough space left on the page 262 || ($tableOnlyManyColumnReport && ($tableOnlyManyColumnReportRowCount == 0 || $tableOnlyManyColumnReportRowCount + $rowCount >= self::MAX_ROW_COUNT)) 263 // or it is a report with both a table and a graph 264 || !$graphOnlyReport && !$tableOnlyReport 265 ) 266 ) { 267 $this->currentPage++; 268 $this->TCPDF->AddPage(); 269 270 // Table-only reports with more than 2 columns are always landscape 271 if ($tableOnlyManyColumnReport) { 272 $tableOnlyManyColumnReportRowCount = 0; 273 $this->orientation = self::LANDSCAPE; 274 } else { 275 // Graph-only reports are always portrait 276 $this->orientation = $graphOnlyReport ? self::PORTRAIT : ($columnCount > $this->maxColumnCountPortraitOrientation ? self::LANDSCAPE : self::PORTRAIT); 277 } 278 279 $this->TCPDF->setPageOrientation($this->orientation, '', $this->bottomMargin); 280 } 281 282 $graphOnlyReportCount = ($graphOnlyReport && $reportHasData) ? ($graphOnlyReportCount + 1) % self::MAX_GRAPH_REPORTS : 0; 283 $tableOnly2ColumnReportCount = ($tableOnly2ColumnReport && $reportHasData) ? ($tableOnly2ColumnReportCount + 1) % self::MAX_2COL_TABLE_REPORTS : 0; 284 $tableOnlyManyColumnReportRowCount = $tableOnlyManyColumnReport ? ($tableOnlyManyColumnReportRowCount + $rowCount) : 0; 285 286 $title = $this->formatText($this->reportMetadata['name']); 287 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportHeaderFontSize); 288 $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]); 289 $this->TCPDF->Bookmark($title); 290 $this->TCPDF->Cell(40, 15, $title); 291 $this->TCPDF->Ln(); 292 $this->TCPDF->SetFont($this->reportFont, '', $this->reportSimpleFontSize); 293 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); 294 } 295 296 private function reportHasData() 297 { 298 return $this->report->getRowsCount() > 0; 299 } 300 301 private function setBorderColor() 302 { 303 $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]); 304 } 305 306 public function renderReport($processedReport) 307 { 308 $this->reportMetadata = $processedReport['metadata']; 309 $this->reportRowsMetadata = $processedReport['reportMetadata']; 310 $this->displayGraph = $processedReport['displayGraph']; 311 $this->evolutionGraph = $processedReport['evolutionGraph']; 312 $this->displayTable = $processedReport['displayTable']; 313 $this->segment = $processedReport['segment']; 314 list($this->report, $this->reportColumns) = self::processTableFormat($this->reportMetadata, $processedReport['reportData'], $processedReport['columns']); 315 316 $this->paintReportHeader(); 317 318 if (!$this->reportHasData()) { 319 $this->paintMessage(Piwik::translate('CoreHome_ThereIsNoDataForThisReport')); 320 return; 321 } 322 323 if ($this->displayGraph) { 324 $this->paintGraph(); 325 } 326 327 if ($this->displayGraph && $this->displayTable) { 328 $this->TCPDF->Ln(5); 329 } 330 331 if ($this->displayTable) { 332 $this->paintReportTableHeader(); 333 $this->paintReportTable(); 334 } 335 } 336 337 private function formatText($text) 338 { 339 return Common::unsanitizeInputValue($text); 340 } 341 342 private function paintReportTable() 343 { 344 //Color and font restoration 345 $this->TCPDF->SetFillColor($this->tableBackgroundColor[0], $this->tableBackgroundColor[1], $this->tableBackgroundColor[2]); 346 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); 347 $this->TCPDF->SetFont(''); 348 349 $fill = true; 350 $url = false; 351 $leftSpacesBeforeLogo = str_repeat(' ', $this->leftSpacesBeforeLogo); 352 353 $logoWidth = $this->logoWidth; 354 $logoHeight = $this->logoHeight; 355 356 $rowsMetadata = $this->reportRowsMetadata->getRows(); 357 358 // Draw a body of report table 359 foreach ($this->report->getRows() as $rowId => $row) { 360 $rowMetrics = $row->getColumns(); 361 $rowMetadata = isset($rowsMetadata[$rowId]) ? $rowsMetadata[$rowId]->getColumns() : array(); 362 if (isset($rowMetadata['url'])) { 363 $url = $rowMetadata['url']; 364 } 365 foreach ($this->reportColumns as $columnId => $columnName) { 366 // Label column 367 if ($columnId == 'label') { 368 $isLogoDisplayable = isset($rowMetadata['logo']); 369 $text = ''; 370 $posX = $this->TCPDF->GetX(); 371 $posY = $this->TCPDF->GetY(); 372 if (isset($rowMetrics[$columnId])) { 373 $text = substr($rowMetrics[$columnId], 0, $this->truncateAfter); 374 if ($isLogoDisplayable) { 375 $text = $leftSpacesBeforeLogo . $text; 376 } 377 } 378 $text = $this->formatText($text); 379 380 $this->TCPDF->Cell($this->labelCellWidth, $this->cellHeight, $text, 'LR', 0, 'L', $fill, $url); 381 382 if ($isLogoDisplayable) { 383 if (isset($rowMetadata['logoWidth'])) { 384 $logoWidth = $rowMetadata['logoWidth']; 385 } 386 if (isset($rowMetadata['logoHeight'])) { 387 $logoHeight = $rowMetadata['logoHeight']; 388 } 389 $restoreY = $this->TCPDF->getY(); 390 $restoreX = $this->TCPDF->getX(); 391 $this->TCPDF->SetY($posY); 392 $this->TCPDF->SetX($posX); 393 $topMargin = 1.3; 394 // Country flags are not very high, force a bigger top margin 395 if ($logoHeight < 16) { 396 $topMargin = 2; 397 } 398 $path = Filesystem::getPathToPiwikRoot() . "/" . $rowMetadata['logo']; 399 if (file_exists($path)) { 400 $this->TCPDF->Image($path, $posX + ($leftMargin = 2), $posY + $topMargin, $logoWidth / 4); 401 } 402 $this->TCPDF->SetXY($restoreX, $restoreY); 403 } 404 } // metrics column 405 else { 406 // No value means 0 407 if (empty($rowMetrics[$columnId])) { 408 $rowMetrics[$columnId] = 0; 409 } 410 $this->TCPDF->Cell($this->cellWidth, $this->cellHeight, NumberFormatter::getInstance()->format($rowMetrics[$columnId]), 'LR', 0, 'L', $fill); 411 } 412 } 413 414 $this->TCPDF->Ln(); 415 416 // Top/Bottom grey border for all cells 417 $this->TCPDF->SetDrawColor($this->rowTopBottomBorder[0], $this->rowTopBottomBorder[1], $this->rowTopBottomBorder[2]); 418 $this->TCPDF->Cell($this->totalWidth, 0, '', 'T'); 419 $this->setBorderColor(); 420 $this->TCPDF->Ln(0.2); 421 422 $fill = !$fill; 423 } 424 } 425 426 private function paintGraph() 427 { 428 $imageGraph = parent::getStaticGraph( 429 $this->reportMetadata, 430 $this->orientation == self::PORTRAIT ? self::IMAGE_GRAPH_WIDTH_PORTRAIT : self::IMAGE_GRAPH_WIDTH_LANDSCAPE, 431 self::IMAGE_GRAPH_HEIGHT, 432 $this->evolutionGraph, 433 $this->segment 434 ); 435 436 $this->TCPDF->Image( 437 '@' . $imageGraph, 438 $x = '', 439 $y = '', 440 $w = 0, 441 $h = 0, 442 $type = '', 443 $link = '', 444 $align = 'N', 445 $resize = false, 446 $dpi = 72, 447 $palign = '', 448 $ismask = false, 449 $imgmask = false, 450 $order = 0, 451 $fitbox = false, 452 $hidden = false, 453 $fitonpage = true, 454 $alt = false, 455 $altimgs = array() 456 ); 457 458 unset($imageGraph); 459 } 460 461 /** 462 * Draw the table header (first row) 463 */ 464 private function paintReportTableHeader() 465 { 466 $initPosX = 10; 467 468 // Get the longest column name 469 $longestColumnName = ''; 470 foreach ($this->reportColumns as $columnName) { 471 if (strlen($columnName) > strlen($longestColumnName)) { 472 $longestColumnName = $columnName; 473 } 474 } 475 476 $columnsCount = count($this->reportColumns); 477 // Computes available column width 478 if ($this->orientation == self::PORTRAIT 479 && $columnsCount <= 3 480 ) { 481 $totalWidth = $this->reportWidthPortrait * 2 / 3; 482 } elseif ($this->orientation == self::LANDSCAPE) { 483 $totalWidth = $this->reportWidthLandscape; 484 } else { 485 $totalWidth = $this->reportWidthPortrait; 486 } 487 $this->totalWidth = $totalWidth; 488 $this->labelCellWidth = max(round(($this->totalWidth / $columnsCount)), $this->minWidthLabelCell); 489 $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / ($columnsCount - 1)); 490 $this->totalWidth = $this->labelCellWidth + ($columnsCount - 1) * $this->cellWidth; 491 492 $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]); 493 $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]); 494 $this->TCPDF->SetLineWidth(.3); 495 $this->setBorderColor(); 496 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle); 497 $this->TCPDF->SetFillColor(255); 498 $this->TCPDF->SetTextColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]); 499 $this->TCPDF->SetDrawColor(255); 500 501 $posY = $this->TCPDF->GetY(); 502 $this->TCPDF->MultiCell($this->cellWidth, $this->cellHeight, $longestColumnName, 1, 'C', true); 503 $maxCellHeight = $this->TCPDF->GetY() - $posY; 504 505 $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]); 506 $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]); 507 $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]); 508 509 $this->TCPDF->SetXY($initPosX, $posY); 510 511 $countColumns = 0; 512 $posX = $initPosX; 513 foreach ($this->reportColumns as $columnName) { 514 $columnName = $this->formatText($columnName); 515 516 //Label column 517 if ($countColumns == 0) { 518 $this->TCPDF->MultiCell($this->labelCellWidth, $maxCellHeight, $columnName, $border = 0, $align = 'L', true); 519 $this->TCPDF->SetXY($posX + $this->labelCellWidth, $posY); 520 } else { 521 $this->TCPDF->MultiCell($this->cellWidth, $maxCellHeight, $columnName, $border = 0, $align = 'L', true); 522 $this->TCPDF->SetXY($posX + $this->cellWidth, $posY); 523 } 524 $countColumns++; 525 $posX = $this->TCPDF->GetX(); 526 } 527 $this->TCPDF->Ln(); 528 $this->TCPDF->SetXY($initPosX, $posY + $maxCellHeight); 529 } 530 531 /** 532 * Prints a message 533 * 534 * @param string $message 535 * @return void 536 */ 537 private function paintMessage($message) 538 { 539 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize); 540 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); 541 $message = $this->formatText($message); 542 $this->TCPDF->Write("1em", $message); 543 $this->TCPDF->Ln(); 544 } 545 546 /** 547 * Get report attachments, ex. graph images 548 * 549 * @param $report 550 * @param $processedReports 551 * @param $prettyDate 552 * @return array 553 */ 554 public function getAttachments($report, $processedReports, $prettyDate) 555 { 556 return array(); 557 } 558} 559