PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.8.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.8.2
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / ReportRenderer / Pdf.php
matomo / app / core / ReportRenderer Last commit date
Csv.php 3 months ago Html.php 1 year ago Pdf.php 3 months ago Tsv.php 3 months ago
Pdf.php
957 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\ReportRenderer;
10
11 use Piwik\Common;
12 use Piwik\Filesystem;
13 use Piwik\NumberFormatter;
14 use Piwik\Piwik;
15 use Piwik\Plugins\CoreAdminHome\CustomLogo;
16 use Piwik\ReportRenderer;
17 use Piwik\TCPDF;
18 use TCPDF_FONTS;
19 /**
20 * @see libs/tcpdf
21 */
22 require_once PIWIK_INCLUDE_PATH . '/plugins/ScheduledReports/config/tcpdf_config.php';
23 /**
24 * PDF report renderer
25 */
26 class Pdf extends ReportRenderer
27 {
28 public const IMAGE_GRAPH_WIDTH_PORTRAIT = 760;
29 public const IMAGE_GRAPH_HEIGHT = 220;
30 public const MAX_ROW_COUNT = 28;
31 public const TABLE_HEADER_ROW_COUNT = 6;
32 public const NO_DATA_ROW_COUNT = 6;
33 public const MAX_GRAPH_REPORTS = 3;
34 public const MAX_2COL_TABLE_REPORTS = 2;
35 public const IMPORT_FONT_PATH = 'plugins/ImageGraph/fonts/unifont.ttf';
36 public const PDF_CONTENT_TYPE = 'pdf';
37 public const PORTRAIT = 'P';
38 private $reportFontStyle = '';
39 private $reportSimpleFontSize = 8.5;
40 private $reportHeaderFontSize = 16;
41 private $cellHeight = 6;
42 private $bottomMargin = 17;
43 private $reportWidthPortrait = 195;
44 private $minWidthLabelCellPortrait = 80;
45 private $minWidthLabelCellPortraitShort = 35;
46 private $logoWidth = 16;
47 private $logoHeight = 16;
48 private $totalWidth;
49 private $cellWidth;
50 private $labelCellWidth;
51 private $maxRowHeight = 13;
52 private $maxLabelCharacter = 135;
53 private $leftSpacesBeforeLogo = 7;
54 private $logoImagePosition = array(10, 40);
55 private $headerBottomPadding = 2;
56 private $headerBottomPaddingShort = 0.5;
57 private $headerTextColor;
58 private $reportTextColor;
59 private $tableHeaderBackgroundColor;
60 private $tableHeaderTextColor;
61 private $tableCellBorderColor;
62 private $tableBackgroundColor;
63 private $rowTopBottomBorder = array(231, 231, 231);
64 private $reportMetadata;
65 private $displayGraph;
66 private $evolutionGraph;
67 private $displayTable;
68 private $segment;
69 private $reportColumns;
70 private $reportRowsMetadata;
71 private $currentPage = 0;
72 private $reportFont = ReportRenderer::DEFAULT_REPORT_FONT_FAMILY;
73 private $TCPDF;
74 private $labelShortContentThreshold = 100;
75 private $columnCellWidths = array();
76 private $labelThirdLinePadding = 4;
77 private $expandedMetricRightPadding = 1.0;
78 private $twoColumnLabelRatio = 0.7;
79 private $threeColumnLabelRatio = 0.65;
80 private $fourColumnLabelRatio = 0.6;
81 private $numColumnsBeforeShrink = 8;
82 private $tableWidthCache = array();
83 private $hasFrontPageBreak = \false;
84 public function __construct()
85 {
86 $this->TCPDF = new TCPDF();
87 $this->headerTextColor = preg_split("/,/", ReportRenderer::REPORT_TITLE_TEXT_COLOR);
88 $this->reportTextColor = preg_split("/,/", ReportRenderer::REPORT_TEXT_COLOR);
89 $this->tableHeaderBackgroundColor = preg_split("/,/", ReportRenderer::TABLE_HEADER_BG_COLOR);
90 $this->tableHeaderTextColor = preg_split("/,/", ReportRenderer::TABLE_HEADER_TEXT_COLOR);
91 $this->tableCellBorderColor = preg_split("/,/", ReportRenderer::TABLE_CELL_BORDER_COLOR);
92 $this->tableBackgroundColor = preg_split("/,/", ReportRenderer::TABLE_BG_COLOR);
93 }
94 public function setLocale($locale)
95 {
96 // WARNING
97 // To make Piwik release smaller, we're deleting some fonts from the Piwik build package.
98 // If you change this code below, make sure that the fonts are NOT deleted from the Piwik package:
99 // https://github.com/piwik/piwik-package/blob/master/scripts/build-package.sh
100 switch ($locale) {
101 case 'bn':
102 case 'hi':
103 $reportFont = 'freesans';
104 break;
105 case 'zh-tw':
106 $reportFont = 'msungstdlight';
107 break;
108 case 'ja':
109 $reportFont = 'kozgopromedium';
110 break;
111 case 'zh-cn':
112 $reportFont = 'stsongstdlight';
113 break;
114 case 'ko':
115 $reportFont = 'hysmyeongjostdmedium';
116 break;
117 case 'ar':
118 $reportFont = 'aealarabiya';
119 break;
120 case 'am':
121 case 'ta':
122 case 'th':
123 $reportFont = 'freeserif';
124 break;
125 case 'te':
126 // not working with bundled fonts
127 case 'en':
128 default:
129 $reportFont = ReportRenderer::DEFAULT_REPORT_FONT_FAMILY;
130 break;
131 }
132 // WARNING: Did you read the warning above?
133 // When user follow the FAQ https://matomo.org/faq/how-to-install/faq_142/, imported unifont font, it will apply across the entire report
134 if (is_file(self::IMPORT_FONT_PATH)) {
135 $reportFont = TCPDF_FONTS::addTTFfont(self::IMPORT_FONT_PATH, 'TrueTypeUnicode');
136 }
137 $this->reportFont = $reportFont;
138 }
139 public function sendToDisk($filename)
140 {
141 $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE);
142 $outputFilename = ReportRenderer::getOutputPath($filename);
143 $this->TCPDF->Output($outputFilename, 'F');
144 return $outputFilename;
145 }
146 public function sendToBrowserDownload($filename)
147 {
148 $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE);
149 $this->TCPDF->Output($filename, 'D');
150 }
151 public function sendToBrowserInline($filename)
152 {
153 $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE);
154 $this->TCPDF->Output($filename, 'I');
155 }
156 public function getRenderedReport()
157 {
158 return $this->TCPDF->Output('', 'S');
159 }
160 public function renderFrontPage($reportTitle, $prettyDate, $description, $reportMetadata, $segment)
161 {
162 $reportTitle = $this->formatText($reportTitle);
163 $dateRange = $this->formatText(Piwik::translate('General_DateRange') . " " . $prettyDate);
164 // footer
165 $this->TCPDF->SetFooterFont(array($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize));
166 $this->TCPDF->SetFooterContent((strlen($reportTitle) > 64 ? substr($reportTitle, 0, 61) . "..." : $reportTitle) . " | " . $dateRange . " | ");
167 // add first page
168 $this->TCPDF->setPrintHeader(\false);
169 $this->TCPDF->AddPage(self::PORTRAIT);
170 $this->TCPDF->AddFont($this->reportFont, '', '', \false);
171 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
172 $this->TCPDF->Bookmark(Piwik::translate('ScheduledReports_FrontPage'));
173 // logo
174 $customLogo = new CustomLogo();
175 $this->TCPDF->Image($customLogo->getLogoUrl(\true), $this->logoImagePosition[0], $this->logoImagePosition[1], 180 / ($factor = 2), 0, $type = '', $link = '', $align = '', $resize = \false, $dpi = 300);
176 $this->TCPDF->Ln(8);
177 // report title
178 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize + 5);
179 $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]);
180 $this->TCPDF->SetXY(10, 119);
181 $this->TCPDF->MultiCell(0, 40, $reportTitle, 0, 'L');
182 // date and period
183 $this->TCPDF->SetXY(10, 152);
184 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize);
185 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
186 $this->TCPDF->MultiCell(0, 40, $dateRange, 0, 'L');
187 // description
188 $this->TCPDF->SetXY(10, 210);
189 $this->TCPDF->Write(1, $this->formatText($description));
190 // segment
191 if ($segment != null) {
192 $this->TCPDF->Ln();
193 $this->TCPDF->Ln();
194 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize - 2);
195 $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]);
196 $this->TCPDF->Write(1, $this->formatText(Piwik::translate('ScheduledReports_CustomVisitorSegment') . ' ' . $segment['name']));
197 }
198 $this->TCPDF->Ln(8);
199 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize);
200 $this->TCPDF->Ln();
201 $this->TCPDF->AddPage(self::PORTRAIT);
202 $this->hasFrontPageBreak = \true;
203 }
204 /**
205 * Generate a header of page.
206 */
207 private function paintReportHeader()
208 {
209 $isAggregateReport = !empty($this->reportMetadata['dimension']);
210 // Graph-only report
211 static $graphOnlyReportCount = 0;
212 $graphOnlyReport = $isAggregateReport && $this->displayGraph && !$this->displayTable;
213 // Table-only report
214 $tableOnlyReport = $isAggregateReport && !$this->displayGraph && $this->displayTable;
215 $columnCount = count($this->reportColumns);
216 // Table-only 2-column report
217 static $tableOnly2ColumnReportCount = 0;
218 $tableOnly2ColumnReport = $tableOnlyReport && $columnCount == 2;
219 // Table-only report with more than 2 columns
220 static $tableOnlyManyColumnReportRowCount = 0;
221 $tableOnlyManyColumnReport = $tableOnlyReport && $columnCount > 3;
222 $reportHasData = $this->reportHasData();
223 $rowCount = $reportHasData ? $this->report->getRowsCount() + self::TABLE_HEADER_ROW_COUNT : self::NO_DATA_ROW_COUNT;
224 $usedFrontPageBreak = \false;
225 if ($this->hasFrontPageBreak && $this->currentPage === 0) {
226 $this->currentPage++;
227 $this->TCPDF->setPageOrientation(self::PORTRAIT, '', $this->bottomMargin);
228 $this->hasFrontPageBreak = \false;
229 $usedFrontPageBreak = \true;
230 }
231 // Only a page break before if the current report has some data
232 if (!$usedFrontPageBreak && $reportHasData && ($this->currentPage == 0 || $graphOnlyReport && $graphOnlyReportCount == 0 || $tableOnly2ColumnReport && $tableOnly2ColumnReportCount == 0 || $tableOnlyManyColumnReport && ($tableOnlyManyColumnReportRowCount == 0 || $tableOnlyManyColumnReportRowCount + $rowCount >= self::MAX_ROW_COUNT) || !$graphOnlyReport && !$tableOnlyReport)) {
233 $this->currentPage++;
234 $this->TCPDF->AddPage();
235 // Scheduled reports should never switch to landscape layouts to keep a consistent portrait output
236 $this->TCPDF->setPageOrientation(self::PORTRAIT, '', $this->bottomMargin);
237 }
238 $graphOnlyReportCount = $graphOnlyReport && $reportHasData ? ($graphOnlyReportCount + 1) % self::MAX_GRAPH_REPORTS : 0;
239 $tableOnly2ColumnReportCount = $tableOnly2ColumnReport && $reportHasData ? ($tableOnly2ColumnReportCount + 1) % self::MAX_2COL_TABLE_REPORTS : 0;
240 $tableOnlyManyColumnReportRowCount = $tableOnlyManyColumnReport ? $tableOnlyManyColumnReportRowCount + $rowCount : 0;
241 $title = $this->formatText($this->reportMetadata['name']);
242 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportHeaderFontSize);
243 $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]);
244 $this->TCPDF->Bookmark($title);
245 $this->TCPDF->Cell(40, 15, $title);
246 $this->TCPDF->Ln();
247 $this->TCPDF->SetFont($this->reportFont, '', $this->reportSimpleFontSize);
248 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
249 }
250 private function reportHasData()
251 {
252 return $this->report->getRowsCount() > 0;
253 }
254 private function setBorderColor()
255 {
256 $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]);
257 }
258 public function renderReport($processedReport)
259 {
260 $this->reportMetadata = $processedReport['metadata'];
261 $this->reportRowsMetadata = $processedReport['reportMetadata'];
262 $this->displayGraph = $processedReport['displayGraph'];
263 $this->evolutionGraph = $processedReport['evolutionGraph'];
264 $this->displayTable = $processedReport['displayTable'];
265 $this->segment = $processedReport['segment'];
266 list($this->report, $this->reportColumns) = self::processTableFormat($this->reportMetadata, $processedReport['reportData'], $processedReport['columns']);
267 $this->paintReportHeader();
268 if (!$this->reportHasData()) {
269 $this->paintMessage(Piwik::translate('CoreHome_ThereIsNoDataForThisReport'));
270 return;
271 }
272 if ($this->displayGraph) {
273 $this->paintGraph();
274 }
275 if ($this->displayGraph && $this->displayTable) {
276 $this->TCPDF->Ln(5);
277 }
278 if ($this->displayTable) {
279 $this->paintReportTableHeader();
280 $this->paintReportTable();
281 }
282 }
283 private function formatText(?string $text) : string
284 {
285 return Common::unsanitizeInputValue($text);
286 }
287 private function limitTextLength(string $text, int $maxLength) : string
288 {
289 if (mb_strlen($text) <= $maxLength) {
290 return $text;
291 }
292 return mb_substr($text, 0, $maxLength - 1) . '';
293 }
294 private function paintReportTable() : void
295 {
296 //Color and font restoration
297 $this->TCPDF->SetFillColor($this->tableBackgroundColor[0], $this->tableBackgroundColor[1], $this->tableBackgroundColor[2]);
298 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
299 $this->TCPDF->SetFont('');
300 $fill = \true;
301 $url = \false;
302 $leftSpacesBeforeLogo = str_repeat(' ', $this->leftSpacesBeforeLogo);
303 $logoWidth = $this->logoWidth;
304 $logoHeight = $this->logoHeight;
305 $rowsMetadata = $this->reportRowsMetadata->getRows();
306 // Draw a body of report table
307 foreach ($this->report->getRows() as $rowId => $row) {
308 $rowMetrics = $row->getColumns();
309 $url = \false;
310 $rowMetadata = isset($rowsMetadata[$rowId]) ? $rowsMetadata[$rowId]->getColumns() : array();
311 if (isset($rowMetadata['url'])) {
312 $url = $rowMetadata['url'];
313 }
314 $metricsPaddingApplied = \false;
315 $previousCellPadding = null;
316 $labelState = $this->computeLabelRenderState($rowMetrics, $rowMetadata, $leftSpacesBeforeLogo, $url);
317 $labelText = $labelState['text'];
318 $rowHeight = $labelState['rowHeight'];
319 $maxHeight = $labelState['maxHeight'];
320 $verticalAlign = $labelState['verticalAlign'];
321 $shouldIncreaseLineHeight = $labelState['shouldIncreaseLineHeight'];
322 $isLogoDisplayable = $labelState['isLogoDisplayable'];
323 if ((float) $this->TCPDF->GetY() + $rowHeight > $this->TCPDF->getPageHeight() - $this->TCPDF->getBreakMargin()) {
324 $this->TCPDF->AddPage();
325 $this->paintReportTableHeader();
326 }
327 foreach ($this->reportColumns as $columnId => $columnName) {
328 // Label column
329 if ($columnId == 'label') {
330 $text = $labelText;
331 $posX = $this->TCPDF->GetX();
332 $posY = $this->TCPDF->GetY();
333 list($previousCellHeightRatio, $previousCellPaddingForLabel) = $this->applyLabelCellStyle($shouldIncreaseLineHeight);
334 $this->TCPDF->MultiCell($this->labelCellWidth, $rowHeight, $text, 'LR', 'L', $fill, 0, '', '', \true, 0, \false, \true, $maxHeight, $verticalAlign);
335 $this->restoreLabelCellStyle($shouldIncreaseLineHeight, $previousCellHeightRatio, $previousCellPaddingForLabel);
336 $this->renderLabelLinkAndLogo($url, $posX, $posY, $rowHeight, $rowMetadata, $isLogoDisplayable, $logoWidth, $logoHeight);
337 } else {
338 // metrics column
339 // No value means 0
340 if (empty($rowMetrics[$columnId])) {
341 $rowMetrics[$columnId] = 0;
342 }
343 $columnWidth = $this->getColumnWidth($columnId);
344 if (!$metricsPaddingApplied) {
345 $previousCellPadding = $this->applyCellPaddingOffset(1, 1);
346 $metricsPaddingApplied = \true;
347 }
348 $this->TCPDF->Cell($columnWidth, $rowHeight, NumberFormatter::getInstance()->format($rowMetrics[$columnId]), 'LR', 0, 'L', $fill, '', 0, \false, 'T', 'T');
349 }
350 }
351 if ($metricsPaddingApplied) {
352 $this->restoreCellPaddingOffset($previousCellPadding);
353 }
354 $this->TCPDF->Ln();
355 // Top/Bottom grey border for all cells
356 $this->TCPDF->SetDrawColor($this->rowTopBottomBorder[0], $this->rowTopBottomBorder[1], $this->rowTopBottomBorder[2]);
357 $this->TCPDF->Cell($this->totalWidth, 0, '', 'T');
358 $this->setBorderColor();
359 $this->TCPDF->Ln(0.2);
360 $fill = !$fill;
361 }
362 }
363 private function getExtraLineHeight(int $labelLineCount) : float
364 {
365 $ratioDelta = $labelLineCount === 2 ? 0.1 : 0.3;
366 return $this->cellHeight * $ratioDelta * ($labelLineCount - 1);
367 }
368 private function computeLabelRenderState(array $rowMetrics, array $rowMetadata, string $leftSpacesBeforeLogo, &$url) : array
369 {
370 $isLogoDisplayable = isset($rowMetadata['logo']);
371 $labelText = '';
372 if (isset($rowMetrics['label'])) {
373 $labelText = trim($rowMetrics['label']);
374 $urlString = $this->isUrl($labelText);
375 if (!$url && $urlString !== \false) {
376 $url = $urlString;
377 }
378 $labelText = $this->limitTextLength($labelText, $this->maxLabelCharacter);
379 if ($isLogoDisplayable) {
380 $labelText = $leftSpacesBeforeLogo . $labelText;
381 }
382 }
383 $labelText = $this->formatText($labelText);
384 $labelLineCount = $this->TCPDF->getNumLines($labelText, $this->labelCellWidth);
385 $shouldIncreaseLineHeight = $isLogoDisplayable && $labelLineCount > 1;
386 $previousCellHeightRatio = null;
387 if ($shouldIncreaseLineHeight) {
388 $previousCellHeightRatio = $this->TCPDF->getCellHeightRatio();
389 $this->TCPDF->setCellHeightRatio($previousCellHeightRatio + 0.3);
390 }
391 $rowHeight = $this->getLabelRowHeight($labelText);
392 if ($shouldIncreaseLineHeight) {
393 $rowHeight += $this->getExtraLineHeight($labelLineCount);
394 $this->TCPDF->setCellHeightRatio($previousCellHeightRatio);
395 }
396 $maxHeight = $shouldIncreaseLineHeight ? $rowHeight : $this->getLabelRowMaxHeight($rowHeight);
397 $verticalAlign = $shouldIncreaseLineHeight ? 'T' : 'M';
398 return array('text' => $labelText, 'rowHeight' => $rowHeight, 'maxHeight' => $maxHeight, 'verticalAlign' => $verticalAlign, 'shouldIncreaseLineHeight' => $shouldIncreaseLineHeight, 'isLogoDisplayable' => $isLogoDisplayable);
399 }
400 private function applyLabelCellStyle(bool $shouldIncreaseLineHeight) : array
401 {
402 if (!$shouldIncreaseLineHeight) {
403 return array(null, null);
404 }
405 $previousCellHeightRatio = $this->TCPDF->getCellHeightRatio();
406 $this->TCPDF->setCellHeightRatio($previousCellHeightRatio + 0.3);
407 $previousCellPaddingForLabel = $this->applyCellPaddingOffset();
408 return array($previousCellHeightRatio, $previousCellPaddingForLabel);
409 }
410 private function restoreLabelCellStyle(bool $shouldIncreaseLineHeight, ?float $previousCellHeightRatio, ?array $previousCellPaddingForLabel) : void
411 {
412 if (!$shouldIncreaseLineHeight || $previousCellHeightRatio === null || $previousCellPaddingForLabel === null) {
413 return;
414 }
415 $this->TCPDF->setCellHeightRatio($previousCellHeightRatio);
416 $this->restoreCellPaddingOffset($previousCellPaddingForLabel);
417 }
418 private function applyCellPaddingOffset(float $left = 0, float $top = 0.8) : array
419 {
420 $previousCellPadding = $this->TCPDF->getCellPaddings();
421 $this->TCPDF->setCellPaddings($previousCellPadding['L'] + $left, $previousCellPadding['T'] + $top, $previousCellPadding['R'], $previousCellPadding['B']);
422 return $previousCellPadding;
423 }
424 private function restoreCellPaddingOffset(array $previousCellPaddings) : void
425 {
426 $this->TCPDF->setCellPaddings($previousCellPaddings['L'], $previousCellPaddings['T'], $previousCellPaddings['R'], $previousCellPaddings['B']);
427 }
428 /**
429 * Gets the row height for a label. This will be the total height including wrapping
430 * but still having a maximum height
431 */
432 private function getLabelRowHeight(string $text) : float
433 {
434 $maxHeight = $this->maxRowHeight;
435 $labelHeight = $this->TCPDF->getStringHeight($this->labelCellWidth, $text);
436 $labelHeight = ceil($labelHeight * 2) / 2;
437 // round up to nearest 0.5 for stable row heights
438 if ($labelHeight > $maxHeight) {
439 return $maxHeight;
440 }
441 if ($labelHeight < $this->cellHeight) {
442 return $this->cellHeight;
443 }
444 return $labelHeight + 1;
445 }
446 /**
447 * @param false|string $url
448 * @param array<string, mixed> $rowMetadata
449 */
450 private function renderLabelLinkAndLogo($url, float $posX, float $posY, float $rowHeight, array $rowMetadata, bool $isLogoDisplayable, float &$logoWidth, float &$logoHeight) : void
451 {
452 if ($url) {
453 $this->TCPDF->Link($posX, $posY, $this->labelCellWidth, $rowHeight, $url);
454 }
455 $this->TCPDF->SetXY($posX + $this->labelCellWidth, $posY);
456 if (!$isLogoDisplayable) {
457 return;
458 }
459 if (isset($rowMetadata['logoWidth'])) {
460 $logoWidth = $rowMetadata['logoWidth'];
461 }
462 if (isset($rowMetadata['logoHeight'])) {
463 $logoHeight = $rowMetadata['logoHeight'];
464 }
465 $restoreY = $this->TCPDF->getY();
466 $restoreX = $this->TCPDF->getX();
467 $this->TCPDF->SetY($posY);
468 $this->TCPDF->SetX($posX);
469 $topMargin = 1.3;
470 // Country flags are not very high, force a bigger top margin
471 if ($logoHeight < 16) {
472 $topMargin = 2;
473 }
474 $path = Filesystem::getPathToPiwikRoot() . "/" . $rowMetadata['logo'];
475 if (file_exists($path)) {
476 $this->TCPDF->Image($path, $posX + ($leftMargin = 2), $posY + $topMargin, $logoWidth / 4);
477 }
478 $this->TCPDF->SetXY($restoreX, $restoreY);
479 }
480 /**
481 * Checks if a string might be a url or not
482 * Will return the string with an 'https' protocol if it is a valid url
483 * @return false|string
484 */
485 private function isUrl(string $value)
486 {
487 $candidate = $value;
488 if (!preg_match('~^[a-z][a-z0-9+.-]*://~i', $candidate)) {
489 $candidate = 'https://' . $candidate;
490 }
491 $host = parse_url($candidate, \PHP_URL_HOST);
492 $isValidHost = $host && strpos($host, '.') !== \false;
493 $isValidUrl = filter_var($candidate, \FILTER_VALIDATE_URL) !== \false && $isValidHost;
494 return $isValidUrl ? $candidate : \false;
495 }
496 /**
497 * This is only useful when label row is 3 lines,
498 * we needed to make max row bigger so that it can be centered vertically better
499 */
500 private function getLabelRowMaxHeight(float $rowHeight) : float
501 {
502 if ($rowHeight >= $this->maxRowHeight) {
503 return $rowHeight + $this->labelThirdLinePadding;
504 }
505 return $rowHeight;
506 }
507 /**
508 * Sets initial label width based on column count and content heuristics.
509 */
510 private function setInitialLabelWidth(int $columnsCount) : void
511 {
512 if ($columnsCount <= 1) {
513 $this->labelCellWidth = $this->totalWidth;
514 $this->cellWidth = 0;
515 return;
516 }
517 if ($columnsCount < 5) {
518 if ($columnsCount === 2) {
519 $labelRatio = $this->twoColumnLabelRatio;
520 } elseif ($columnsCount === 3) {
521 $labelRatio = $this->threeColumnLabelRatio;
522 } else {
523 $labelRatio = $this->fourColumnLabelRatio;
524 }
525 $metricColumns = $columnsCount - 1;
526 $this->labelCellWidth = max((int) round($this->totalWidth * $labelRatio), $this->minWidthLabelCellPortrait);
527 $this->cellWidth = (int) round(($this->totalWidth - $this->labelCellWidth) / $metricColumns);
528 $this->totalWidth = $this->labelCellWidth + $metricColumns * $this->cellWidth;
529 return;
530 }
531 if (!$this->reportHasData() || !$this->shouldUseShortLabelWidth()) {
532 return;
533 }
534 $this->labelCellWidth = $this->minWidthLabelCellPortraitShort;
535 $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / ($columnsCount - 1));
536 $this->totalWidth = $this->labelCellWidth + ($columnsCount - 1) * $this->cellWidth;
537 }
538 private function shrinkLabelWidthForSingleLineLabels(int $columnsCount) : void
539 {
540 if ($columnsCount <= $this->numColumnsBeforeShrink || !$this->reportHasData()) {
541 return;
542 }
543 $maxLabelWidth = $this->getMaxSingleLineLabelWidth();
544 if ($maxLabelWidth === null) {
545 return;
546 }
547 $maxLabelWidth = max($maxLabelWidth, $this->minWidthLabelCellPortraitShort);
548 if ($maxLabelWidth >= $this->labelCellWidth) {
549 return;
550 }
551 $metricColumns = $columnsCount - 1;
552 $this->labelCellWidth = $maxLabelWidth;
553 $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / $metricColumns);
554 $this->totalWidth = $this->labelCellWidth + $metricColumns * $this->cellWidth;
555 }
556 private function capLabelWidthForManyColumns(int $columnsCount) : void
557 {
558 if ($columnsCount <= $this->numColumnsBeforeShrink) {
559 return;
560 }
561 $maxLabelWidth = round($this->totalWidth * 0.35, 2);
562 if ($this->labelCellWidth <= $maxLabelWidth) {
563 return;
564 }
565 $metricColumns = $columnsCount - 1;
566 $this->labelCellWidth = $maxLabelWidth;
567 $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / $metricColumns);
568 $this->totalWidth = $this->labelCellWidth + $metricColumns * $this->cellWidth;
569 }
570 private function getMaxSingleLineLabelWidth() : ?float
571 {
572 if (!empty($this->tableWidthCache['maxSingleLineLabelWidthFor']) && (float) $this->tableWidthCache['maxSingleLineLabelWidthFor'] === (float) $this->labelCellWidth) {
573 return $this->tableWidthCache['maxSingleLineLabelWidth'];
574 }
575 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
576 $rowsMetadata = array();
577 if (!empty($this->reportRowsMetadata)) {
578 $rowsMetadata = $this->reportRowsMetadata->getRows();
579 }
580 $maxWidth = 0.0;
581 foreach ($this->report->getRows() as $rowId => $row) {
582 $label = $row->getColumn('label');
583 if ($label === \false || $label === null) {
584 continue;
585 }
586 $labelText = $this->buildFormattedLabelTextForRow($rowId, (string) $label, $rowsMetadata, $this->maxLabelCharacter);
587 if ($this->TCPDF->getNumLines($labelText, $this->labelCellWidth) > 1) {
588 return null;
589 }
590 $width = $this->TCPDF->GetStringWidth($labelText);
591 if ($width > $maxWidth) {
592 $maxWidth = $width;
593 }
594 }
595 if ($maxWidth <= 0) {
596 $this->tableWidthCache['maxSingleLineLabelWidth'] = null;
597 $this->tableWidthCache['maxSingleLineLabelWidthFor'] = $this->labelCellWidth;
598 return null;
599 }
600 $padding = $this->TCPDF->getCellPaddings();
601 $maxWidth = $maxWidth + $padding['L'] + $padding['R'];
602 $this->tableWidthCache['maxSingleLineLabelWidth'] = $maxWidth;
603 $this->tableWidthCache['maxSingleLineLabelWidthFor'] = $this->labelCellWidth;
604 return $maxWidth;
605 }
606 private function getColumnWidth(string $columnId) : float
607 {
608 if (isset($this->columnCellWidths[$columnId])) {
609 return (float) $this->columnCellWidths[$columnId];
610 }
611 if ($columnId === 'label') {
612 return (float) $this->labelCellWidth;
613 }
614 return (float) $this->cellWidth;
615 }
616 /**
617 * This function will try to show all values for selected metric columns.
618 * Will adjust other column widths to accommodate this
619 */
620 private function adjustMetricColumnWidthsToContent() : void
621 {
622 if (!$this->reportHasData()) {
623 return;
624 }
625 $metricColumnsToAdjust = array('revenue', 'ecommerce_revenue', 'avg_time_on_site');
626 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
627 foreach ($metricColumnsToAdjust as $columnId) {
628 $this->adjustMetricColumnWidthToContent($columnId);
629 }
630 }
631 /**
632 * This function will try to adjust column width based on the content.
633 * This will try to make other columns smaller to accommodate this
634 */
635 private function adjustMetricColumnWidthToContent(string $columnId) : void
636 {
637 if (!array_key_exists($columnId, $this->reportColumns) || !isset($this->columnCellWidths[$columnId])) {
638 return;
639 }
640 $requiredWidth = $this->getMaxFormattedColumnWidth($columnId);
641 if ($requiredWidth <= 0) {
642 return;
643 }
644 $currentWidth = $this->columnCellWidths[$columnId];
645 if ($requiredWidth <= $currentWidth) {
646 return;
647 }
648 $additionalWidth = $requiredWidth - $currentWidth;
649 if ($additionalWidth <= 1.0) {
650 $requiredWidth += $this->expandedMetricRightPadding;
651 $additionalWidth = $requiredWidth - $currentWidth;
652 }
653 $remainingWidthToGain = $additionalWidth;
654 $minMetricWidth = 10;
655 $adjustableColumns = array();
656 foreach ($this->columnCellWidths as $otherColumnId => $width) {
657 if ($otherColumnId === 'label' || $otherColumnId === $columnId) {
658 continue;
659 }
660 if ($width <= $minMetricWidth) {
661 continue;
662 }
663 $adjustableColumns[$otherColumnId] = $width;
664 }
665 while ($remainingWidthToGain > 0 && !empty($adjustableColumns)) {
666 $share = $remainingWidthToGain / count($adjustableColumns);
667 $updatedColumns = array();
668 foreach ($adjustableColumns as $otherColumnId => $availableWidth) {
669 $maxReducible = $availableWidth - $minMetricWidth;
670 if ($maxReducible <= 0) {
671 continue;
672 }
673 $reduction = min($maxReducible, $share);
674 if ($reduction <= 0) {
675 continue;
676 }
677 $this->columnCellWidths[$otherColumnId] -= $reduction;
678 $remainingWidthToGain -= $reduction;
679 $newWidth = $availableWidth - $reduction;
680 if ($newWidth > $minMetricWidth && $remainingWidthToGain > 0) {
681 $updatedColumns[$otherColumnId] = $newWidth;
682 }
683 if ($remainingWidthToGain <= 0) {
684 break 2;
685 }
686 }
687 $adjustableColumns = $updatedColumns;
688 }
689 $appliedWidth = $additionalWidth - $remainingWidthToGain;
690 if ($appliedWidth <= 0) {
691 return;
692 }
693 $this->columnCellWidths[$columnId] += $appliedWidth;
694 }
695 /**
696 * Computes maximum column width for a given metric column
697 */
698 private function getMaxFormattedColumnWidth(string $columnId) : float
699 {
700 if (!empty($this->tableWidthCache) && isset($this->tableWidthCache['metricMaxWidths'][$columnId])) {
701 $maxWidth = $this->tableWidthCache['metricMaxWidths'][$columnId];
702 if ($maxWidth <= 0) {
703 return 0;
704 }
705 return $maxWidth + 2;
706 }
707 $maxWidth = 0;
708 foreach ($this->report->getRows() as $row) {
709 $value = $row->getColumn($columnId);
710 if ($value === \false || $value === null) {
711 continue;
712 }
713 $formattedValue = NumberFormatter::getInstance()->format($value);
714 $width = $this->TCPDF->GetStringWidth($formattedValue);
715 if ($width > $maxWidth) {
716 $maxWidth = $width;
717 }
718 }
719 if ($maxWidth <= 0) {
720 return 0;
721 }
722 return $maxWidth + 2;
723 }
724 private function initializeTableWidthCache() : void
725 {
726 $this->tableWidthCache = array('ready' => \true, 'labelTooLong' => \false, 'labelFitsShortWidth' => \true, 'maxLabelLength' => 0, 'metricMaxWidths' => array(), 'maxSingleLineLabelWidth' => null, 'maxSingleLineLabelWidthFor' => null);
727 if (!$this->reportHasData()) {
728 return;
729 }
730 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
731 $maxCharacters = $this->maxLabelCharacter;
732 $rowsMetadata = array();
733 if (!empty($this->reportRowsMetadata)) {
734 $rowsMetadata = $this->reportRowsMetadata->getRows();
735 }
736 $metricColumnsToAdjust = array('revenue', 'ecommerce_revenue', 'avg_time_on_site');
737 foreach ($metricColumnsToAdjust as $columnId) {
738 $this->tableWidthCache['metricMaxWidths'][$columnId] = 0.0;
739 }
740 foreach ($this->report->getRows() as $rowId => $row) {
741 $label = $row->getColumn('label');
742 if ($label !== \false && $label !== null) {
743 $rawLabel = (string) $label;
744 $length = mb_strlen($rawLabel);
745 if ($length > $this->tableWidthCache['maxLabelLength']) {
746 $this->tableWidthCache['maxLabelLength'] = $length;
747 }
748 if ($length > $maxCharacters) {
749 $this->tableWidthCache['labelTooLong'] = \true;
750 }
751 if ($this->tableWidthCache['labelFitsShortWidth']) {
752 $formattedLabel = $this->buildFormattedLabelTextForRow($rowId, $rawLabel, $rowsMetadata, $maxCharacters);
753 if ($this->TCPDF->getNumLines($formattedLabel, $this->minWidthLabelCellPortraitShort) > 1) {
754 $this->tableWidthCache['labelFitsShortWidth'] = \false;
755 }
756 }
757 }
758 foreach ($metricColumnsToAdjust as $columnId) {
759 if (!array_key_exists($columnId, $this->reportColumns)) {
760 continue;
761 }
762 $value = $row->getColumn($columnId);
763 if ($value === \false || $value === null) {
764 continue;
765 }
766 $formattedValue = NumberFormatter::getInstance()->format($value);
767 $width = $this->TCPDF->GetStringWidth($formattedValue);
768 if ($width > $this->tableWidthCache['metricMaxWidths'][$columnId]) {
769 $this->tableWidthCache['metricMaxWidths'][$columnId] = $width;
770 }
771 }
772 }
773 }
774 /**
775 * Will check if label column could use a shorter width.
776 * This is done so that we can fit more metrics in the same row for data table with no label that is too long
777 */
778 private function shouldUseShortLabelWidth() : bool
779 {
780 if (empty($this->tableWidthCache) || empty($this->tableWidthCache['ready'])) {
781 $this->initializeTableWidthCache();
782 }
783 if (empty($this->tableWidthCache['maxLabelLength'])) {
784 return \false;
785 }
786 if (!empty($this->tableWidthCache['labelTooLong'])) {
787 return \false;
788 }
789 if ($this->tableWidthCache['maxLabelLength'] >= $this->labelShortContentThreshold) {
790 return \false;
791 }
792 if (empty($this->tableWidthCache['labelFitsShortWidth'])) {
793 return \false;
794 }
795 return $this->tableWidthCache['maxLabelLength'] > 0;
796 }
797 private function buildFormattedLabelTextForRow(int $rowId, string $label, array $rowsMetadata, int $maxCharacters) : string
798 {
799 $labelText = trim($label);
800 $labelText = $this->limitTextLength($labelText, $maxCharacters);
801 if (isset($rowsMetadata[$rowId])) {
802 $rowMeta = $rowsMetadata[$rowId]->getColumns();
803 if (isset($rowMeta['logo'])) {
804 $labelText = str_repeat(' ', $this->leftSpacesBeforeLogo) . $labelText;
805 }
806 }
807 return $this->formatText($labelText);
808 }
809 private function paintGraph() : void
810 {
811 $imageGraph = parent::getStaticGraph($this->reportMetadata, self::IMAGE_GRAPH_WIDTH_PORTRAIT, self::IMAGE_GRAPH_HEIGHT, $this->evolutionGraph, $this->segment);
812 $this->TCPDF->Image('@' . $imageGraph, $x = '', $y = '', $w = 0, $h = 0, $type = '', $link = '', $align = 'N', $resize = \false, $dpi = 72, $palign = '', $ismask = \false, $imgmask = \false, $order = 0, $fitbox = \false, $hidden = \false, $fitonpage = \true, $alt = \false, $altimgs = array());
813 unset($imageGraph);
814 }
815 /**
816 * Draw the table header (first row)
817 */
818 private function paintReportTableHeader() : void
819 {
820 $initPosX = 10;
821 $this->initializeTableColumnWidths();
822 $this->setupHeaderRenderingStyle();
823 $posY = $this->TCPDF->GetY();
824 list($columnData, $maxCellHeight) = $this->buildHeaderColumnData();
825 $this->TCPDF->SetXY($initPosX, $posY);
826 $this->renderHeaderColumns($columnData, $initPosX, $posY, $maxCellHeight);
827 $extraSpacing = 1;
828 $this->TCPDF->Ln($extraSpacing);
829 $this->TCPDF->SetXY($initPosX, $posY + $maxCellHeight + $extraSpacing);
830 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
831 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
832 }
833 /**
834 * Will initialize table column widths,
835 * this will include adjusting label and revenue columns
836 */
837 private function initializeTableColumnWidths() : void
838 {
839 $columnsCount = count($this->reportColumns);
840 if ($columnsCount === 0) {
841 return;
842 }
843 $this->totalWidth = $this->reportWidthPortrait;
844 $minLabelWidth = $this->minWidthLabelCellPortrait;
845 $this->labelCellWidth = max(round($this->totalWidth / $columnsCount), $minLabelWidth);
846 $metricColumns = max(1, $columnsCount - 1);
847 $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / $metricColumns);
848 $this->totalWidth = $this->labelCellWidth + $metricColumns * $this->cellWidth;
849 $this->initializeTableWidthCache();
850 $this->setInitialLabelWidth($columnsCount);
851 $this->capLabelWidthForManyColumns($columnsCount);
852 $this->shrinkLabelWidthForSingleLineLabels($columnsCount);
853 $this->columnCellWidths = array();
854 foreach ($this->reportColumns as $columnId => $_) {
855 $this->columnCellWidths[$columnId] = $columnId === 'label' ? $this->labelCellWidth : $this->cellWidth;
856 }
857 $this->adjustMetricColumnWidthsToContent();
858 $this->totalWidth = array_sum($this->columnCellWidths);
859 }
860 private function setupHeaderRenderingStyle() : void
861 {
862 $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]);
863 $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]);
864 $this->TCPDF->SetLineWidth(0.3);
865 $this->setBorderColor();
866 $this->TCPDF->SetFont($this->reportFont, 'B');
867 $this->TCPDF->SetFillColor(255);
868 $this->TCPDF->SetTextColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]);
869 $this->TCPDF->SetDrawColor(255);
870 }
871 /**
872 * Will adjust table headers based on their column name and make them be closer to the table
873 * @return array
874 */
875 private function buildHeaderColumnData() : array
876 {
877 $columnData = array();
878 $maxCellHeight = $this->cellHeight;
879 foreach ($this->reportColumns as $columnId => $columnName) {
880 $columnName = $this->formatText($columnName);
881 $columnWidth = $this->getColumnWidth($columnId);
882 $textHeight = $this->TCPDF->getStringHeight($columnWidth, $columnName);
883 $cellHeight = max($textHeight, $this->cellHeight);
884 $columnData[] = array('text' => $columnName, 'width' => $columnWidth, 'height' => $cellHeight, 'textHeight' => $textHeight);
885 if ($cellHeight > $maxCellHeight) {
886 $maxCellHeight = $cellHeight;
887 }
888 }
889 return array($columnData, $maxCellHeight);
890 }
891 private function renderHeaderColumns(array $columnData, float $initPosX, float $posY, float $maxCellHeight) : void
892 {
893 $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]);
894 $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]);
895 $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]);
896 $posX = $initPosX;
897 foreach ($columnData as $columnInfo) {
898 $columnWidth = $columnInfo['width'];
899 $textHeight = $columnInfo['textHeight'];
900 $this->TCPDF->Rect($posX, $posY, $columnWidth, $maxCellHeight, 'F');
901 $textPosY = $this->calculateHeaderTextY($posY, $maxCellHeight, $textHeight);
902 $this->TCPDF->SetXY($posX, $textPosY);
903 $this->TCPDF->MultiCell($columnWidth, $this->cellHeight, $columnInfo['text'], 0, 'L', \false, 0, '', '', \true, 0, \false, \true, 0, 'T');
904 $this->TCPDF->SetXY($posX + $columnWidth, $posY);
905 $posX = $this->TCPDF->GetX();
906 }
907 }
908 private function calculateHeaderTextY(float $posY, float $maxCellHeight, float $textHeight) : float
909 {
910 if ($textHeight <= 0) {
911 $textHeight = $this->cellHeight;
912 }
913 $bottomPadding = $this->headerBottomPadding;
914 if ($textHeight <= $this->cellHeight) {
915 $bottomPadding = 0;
916 } elseif ($textHeight <= $this->cellHeight * 2) {
917 $bottomPadding = $this->headerBottomPaddingShort;
918 }
919 $availableSpace = max(0, $maxCellHeight - $textHeight);
920 $textPosY = $posY + $availableSpace - $bottomPadding;
921 $minY = $posY;
922 $maxY = $posY + $maxCellHeight - $textHeight;
923 if ($textPosY < $minY) {
924 return $minY;
925 }
926 if ($textPosY > $maxY) {
927 return $maxY;
928 }
929 return $textPosY;
930 }
931 /**
932 * Prints a message
933 *
934 * @param string $message
935 */
936 private function paintMessage($message) : void
937 {
938 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
939 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
940 $message = $this->formatText($message);
941 $this->TCPDF->Write("1em", $message);
942 $this->TCPDF->Ln();
943 }
944 /**
945 * Get report attachments, ex. graph images
946 *
947 * @param $report
948 * @param $processedReports
949 * @param $prettyDate
950 * @return array
951 */
952 public function getAttachments($report, $processedReports, $prettyDate) : array
953 {
954 return array();
955 }
956 }
957