PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.13.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.13.0
5.13.0 5.12.1 5.12.0 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 1 month ago Html.php 1 year ago Pdf.php 5 days ago Tsv.php 1 month ago
Pdf.php
959 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 self::checkStreamingToBrowserIsAllowed();
149 $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE);
150 $this->TCPDF->Output($filename, 'D');
151 }
152 public function sendToBrowserInline($filename)
153 {
154 self::checkStreamingToBrowserIsAllowed();
155 $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE);
156 $this->TCPDF->Output($filename, 'I');
157 }
158 public function getRenderedReport()
159 {
160 return $this->TCPDF->Output('', 'S');
161 }
162 public function renderFrontPage($reportTitle, $prettyDate, $description, $reportMetadata, $segment)
163 {
164 $reportTitle = $this->formatText($reportTitle);
165 $dateRange = $this->formatText(Piwik::translate('General_DateRange') . " " . $prettyDate);
166 // footer
167 $this->TCPDF->SetFooterFont(array($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize));
168 $this->TCPDF->SetFooterContent((strlen($reportTitle) > 64 ? substr($reportTitle, 0, 61) . "..." : $reportTitle) . " | " . $dateRange . " | ");
169 // add first page
170 $this->TCPDF->setPrintHeader(\false);
171 $this->TCPDF->AddPage(self::PORTRAIT);
172 $this->TCPDF->AddFont($this->reportFont, '', '', \false);
173 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
174 $this->TCPDF->Bookmark(Piwik::translate('ScheduledReports_FrontPage'));
175 // logo
176 $customLogo = new CustomLogo();
177 $this->TCPDF->Image($customLogo->getLogoUrl(\true), $this->logoImagePosition[0], $this->logoImagePosition[1], 180 / ($factor = 2), 0, $type = '', $link = '', $align = '', $resize = \false, $dpi = 300);
178 $this->TCPDF->Ln(8);
179 // report title
180 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize + 5);
181 $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]);
182 $this->TCPDF->SetXY(10, 119);
183 $this->TCPDF->MultiCell(0, 40, $reportTitle, 0, 'L');
184 // date and period
185 $this->TCPDF->SetXY(10, 152);
186 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize);
187 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
188 $this->TCPDF->MultiCell(0, 40, $dateRange, 0, 'L');
189 // description
190 $this->TCPDF->SetXY(10, 210);
191 $this->TCPDF->Write(1, $this->formatText($description));
192 // segment
193 if ($segment != null) {
194 $this->TCPDF->Ln();
195 $this->TCPDF->Ln();
196 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize - 2);
197 $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]);
198 $this->TCPDF->Write(1, $this->formatText(Piwik::translate('ScheduledReports_CustomVisitorSegment') . ' ' . $segment['name']));
199 }
200 $this->TCPDF->Ln(8);
201 $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize);
202 $this->TCPDF->Ln();
203 $this->TCPDF->AddPage(self::PORTRAIT);
204 $this->hasFrontPageBreak = \true;
205 }
206 /**
207 * Generate a header of page.
208 */
209 private function paintReportHeader()
210 {
211 $isAggregateReport = !empty($this->reportMetadata['dimension']);
212 // Graph-only report
213 static $graphOnlyReportCount = 0;
214 $graphOnlyReport = $isAggregateReport && $this->displayGraph && !$this->displayTable;
215 // Table-only report
216 $tableOnlyReport = $isAggregateReport && !$this->displayGraph && $this->displayTable;
217 $columnCount = count($this->reportColumns);
218 // Table-only 2-column report
219 static $tableOnly2ColumnReportCount = 0;
220 $tableOnly2ColumnReport = $tableOnlyReport && $columnCount == 2;
221 // Table-only report with more than 2 columns
222 static $tableOnlyManyColumnReportRowCount = 0;
223 $tableOnlyManyColumnReport = $tableOnlyReport && $columnCount > 3;
224 $reportHasData = $this->reportHasData();
225 $rowCount = $reportHasData ? $this->report->getRowsCount() + self::TABLE_HEADER_ROW_COUNT : self::NO_DATA_ROW_COUNT;
226 $usedFrontPageBreak = \false;
227 if ($this->hasFrontPageBreak && $this->currentPage === 0) {
228 $this->currentPage++;
229 $this->TCPDF->setPageOrientation(self::PORTRAIT, '', $this->bottomMargin);
230 $this->hasFrontPageBreak = \false;
231 $usedFrontPageBreak = \true;
232 }
233 // Only a page break before if the current report has some data
234 if (!$usedFrontPageBreak && $reportHasData && ($this->currentPage == 0 || $graphOnlyReport && $graphOnlyReportCount == 0 || $tableOnly2ColumnReport && $tableOnly2ColumnReportCount == 0 || $tableOnlyManyColumnReport && ($tableOnlyManyColumnReportRowCount == 0 || $tableOnlyManyColumnReportRowCount + $rowCount >= self::MAX_ROW_COUNT) || !$graphOnlyReport && !$tableOnlyReport)) {
235 $this->currentPage++;
236 $this->TCPDF->AddPage();
237 // Scheduled reports should never switch to landscape layouts to keep a consistent portrait output
238 $this->TCPDF->setPageOrientation(self::PORTRAIT, '', $this->bottomMargin);
239 }
240 $graphOnlyReportCount = $graphOnlyReport && $reportHasData ? ($graphOnlyReportCount + 1) % self::MAX_GRAPH_REPORTS : 0;
241 $tableOnly2ColumnReportCount = $tableOnly2ColumnReport && $reportHasData ? ($tableOnly2ColumnReportCount + 1) % self::MAX_2COL_TABLE_REPORTS : 0;
242 $tableOnlyManyColumnReportRowCount = $tableOnlyManyColumnReport ? $tableOnlyManyColumnReportRowCount + $rowCount : 0;
243 $title = $this->formatText($this->reportMetadata['name']);
244 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportHeaderFontSize);
245 $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]);
246 $this->TCPDF->Bookmark($title);
247 $this->TCPDF->Cell(40, 15, $title);
248 $this->TCPDF->Ln();
249 $this->TCPDF->SetFont($this->reportFont, '', $this->reportSimpleFontSize);
250 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
251 }
252 private function reportHasData()
253 {
254 return $this->report->getRowsCount() > 0;
255 }
256 private function setBorderColor()
257 {
258 $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]);
259 }
260 public function renderReport($processedReport)
261 {
262 $this->reportMetadata = $processedReport['metadata'];
263 $this->reportRowsMetadata = $processedReport['reportMetadata'];
264 $this->displayGraph = $processedReport['displayGraph'];
265 $this->evolutionGraph = $processedReport['evolutionGraph'];
266 $this->displayTable = $processedReport['displayTable'];
267 $this->segment = $processedReport['segment'];
268 list($this->report, $this->reportColumns) = self::processTableFormat($this->reportMetadata, $processedReport['reportData'], $processedReport['columns']);
269 $this->paintReportHeader();
270 if (!$this->reportHasData()) {
271 $this->paintMessage(Piwik::translate('CoreHome_ThereIsNoDataForThisReport'));
272 return;
273 }
274 if ($this->displayGraph) {
275 $this->paintGraph();
276 }
277 if ($this->displayGraph && $this->displayTable) {
278 $this->TCPDF->Ln(5);
279 }
280 if ($this->displayTable) {
281 $this->paintReportTableHeader();
282 $this->paintReportTable();
283 }
284 }
285 private function formatText(?string $text) : string
286 {
287 return Common::unsanitizeInputValue($text);
288 }
289 private function limitTextLength(string $text, int $maxLength) : string
290 {
291 if (mb_strlen($text) <= $maxLength) {
292 return $text;
293 }
294 return mb_substr($text, 0, $maxLength - 1) . '';
295 }
296 private function paintReportTable() : void
297 {
298 //Color and font restoration
299 $this->TCPDF->SetFillColor($this->tableBackgroundColor[0], $this->tableBackgroundColor[1], $this->tableBackgroundColor[2]);
300 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
301 $this->TCPDF->SetFont('');
302 $fill = \true;
303 $url = \false;
304 $leftSpacesBeforeLogo = str_repeat(' ', $this->leftSpacesBeforeLogo);
305 $logoWidth = $this->logoWidth;
306 $logoHeight = $this->logoHeight;
307 $rowsMetadata = $this->reportRowsMetadata->getRows();
308 // Draw a body of report table
309 foreach ($this->report->getRows() as $rowId => $row) {
310 $rowMetrics = $row->getColumns();
311 $url = \false;
312 $rowMetadata = isset($rowsMetadata[$rowId]) ? $rowsMetadata[$rowId]->getColumns() : array();
313 if (isset($rowMetadata['url'])) {
314 $url = $rowMetadata['url'];
315 }
316 $metricsPaddingApplied = \false;
317 $previousCellPadding = null;
318 $labelState = $this->computeLabelRenderState($rowMetrics, $rowMetadata, $leftSpacesBeforeLogo, $url);
319 $labelText = $labelState['text'];
320 $rowHeight = $labelState['rowHeight'];
321 $maxHeight = $labelState['maxHeight'];
322 $verticalAlign = $labelState['verticalAlign'];
323 $shouldIncreaseLineHeight = $labelState['shouldIncreaseLineHeight'];
324 $isLogoDisplayable = $labelState['isLogoDisplayable'];
325 if ((float) $this->TCPDF->GetY() + $rowHeight > $this->TCPDF->getPageHeight() - $this->TCPDF->getBreakMargin()) {
326 $this->TCPDF->AddPage();
327 $this->paintReportTableHeader();
328 }
329 foreach ($this->reportColumns as $columnId => $columnName) {
330 // Label column
331 if ($columnId == 'label') {
332 $text = $labelText;
333 $posX = $this->TCPDF->GetX();
334 $posY = $this->TCPDF->GetY();
335 list($previousCellHeightRatio, $previousCellPaddingForLabel) = $this->applyLabelCellStyle($shouldIncreaseLineHeight);
336 $this->TCPDF->MultiCell($this->labelCellWidth, $rowHeight, $text, 'LR', 'L', $fill, 0, '', '', \true, 0, \false, \true, $maxHeight, $verticalAlign);
337 $this->restoreLabelCellStyle($shouldIncreaseLineHeight, $previousCellHeightRatio, $previousCellPaddingForLabel);
338 $this->renderLabelLinkAndLogo($url, $posX, $posY, $rowHeight, $rowMetadata, $isLogoDisplayable, $logoWidth, $logoHeight);
339 } else {
340 // metrics column
341 // No value means 0
342 if (empty($rowMetrics[$columnId])) {
343 $rowMetrics[$columnId] = 0;
344 }
345 $columnWidth = $this->getColumnWidth($columnId);
346 if (!$metricsPaddingApplied) {
347 $previousCellPadding = $this->applyCellPaddingOffset(1, 1);
348 $metricsPaddingApplied = \true;
349 }
350 $this->TCPDF->Cell($columnWidth, $rowHeight, NumberFormatter::getInstance()->format($rowMetrics[$columnId]), 'LR', 0, 'L', $fill, '', 0, \false, 'T', 'T');
351 }
352 }
353 if ($metricsPaddingApplied) {
354 $this->restoreCellPaddingOffset($previousCellPadding);
355 }
356 $this->TCPDF->Ln();
357 // Top/Bottom grey border for all cells
358 $this->TCPDF->SetDrawColor($this->rowTopBottomBorder[0], $this->rowTopBottomBorder[1], $this->rowTopBottomBorder[2]);
359 $this->TCPDF->Cell($this->totalWidth, 0, '', 'T');
360 $this->setBorderColor();
361 $this->TCPDF->Ln(0.2);
362 $fill = !$fill;
363 }
364 }
365 private function getExtraLineHeight(int $labelLineCount) : float
366 {
367 $ratioDelta = $labelLineCount === 2 ? 0.1 : 0.3;
368 return $this->cellHeight * $ratioDelta * ($labelLineCount - 1);
369 }
370 private function computeLabelRenderState(array $rowMetrics, array $rowMetadata, string $leftSpacesBeforeLogo, &$url) : array
371 {
372 $isLogoDisplayable = isset($rowMetadata['logo']);
373 $labelText = '';
374 if (isset($rowMetrics['label'])) {
375 $labelText = trim($rowMetrics['label']);
376 $urlString = $this->isUrl($labelText);
377 if (!$url && $urlString !== \false) {
378 $url = $urlString;
379 }
380 $labelText = $this->limitTextLength($labelText, $this->maxLabelCharacter);
381 if ($isLogoDisplayable) {
382 $labelText = $leftSpacesBeforeLogo . $labelText;
383 }
384 }
385 $labelText = $this->formatText($labelText);
386 $labelLineCount = $this->TCPDF->getNumLines($labelText, $this->labelCellWidth);
387 $shouldIncreaseLineHeight = $isLogoDisplayable && $labelLineCount > 1;
388 $previousCellHeightRatio = null;
389 if ($shouldIncreaseLineHeight) {
390 $previousCellHeightRatio = $this->TCPDF->getCellHeightRatio();
391 $this->TCPDF->setCellHeightRatio($previousCellHeightRatio + 0.3);
392 }
393 $rowHeight = $this->getLabelRowHeight($labelText);
394 if ($shouldIncreaseLineHeight) {
395 $rowHeight += $this->getExtraLineHeight($labelLineCount);
396 $this->TCPDF->setCellHeightRatio($previousCellHeightRatio);
397 }
398 $maxHeight = $shouldIncreaseLineHeight ? $rowHeight : $this->getLabelRowMaxHeight($rowHeight);
399 $verticalAlign = $shouldIncreaseLineHeight ? 'T' : 'M';
400 return array('text' => $labelText, 'rowHeight' => $rowHeight, 'maxHeight' => $maxHeight, 'verticalAlign' => $verticalAlign, 'shouldIncreaseLineHeight' => $shouldIncreaseLineHeight, 'isLogoDisplayable' => $isLogoDisplayable);
401 }
402 private function applyLabelCellStyle(bool $shouldIncreaseLineHeight) : array
403 {
404 if (!$shouldIncreaseLineHeight) {
405 return array(null, null);
406 }
407 $previousCellHeightRatio = $this->TCPDF->getCellHeightRatio();
408 $this->TCPDF->setCellHeightRatio($previousCellHeightRatio + 0.3);
409 $previousCellPaddingForLabel = $this->applyCellPaddingOffset();
410 return array($previousCellHeightRatio, $previousCellPaddingForLabel);
411 }
412 private function restoreLabelCellStyle(bool $shouldIncreaseLineHeight, ?float $previousCellHeightRatio, ?array $previousCellPaddingForLabel) : void
413 {
414 if (!$shouldIncreaseLineHeight || $previousCellHeightRatio === null || $previousCellPaddingForLabel === null) {
415 return;
416 }
417 $this->TCPDF->setCellHeightRatio($previousCellHeightRatio);
418 $this->restoreCellPaddingOffset($previousCellPaddingForLabel);
419 }
420 private function applyCellPaddingOffset(float $left = 0, float $top = 0.8) : array
421 {
422 $previousCellPadding = $this->TCPDF->getCellPaddings();
423 $this->TCPDF->setCellPaddings($previousCellPadding['L'] + $left, $previousCellPadding['T'] + $top, $previousCellPadding['R'], $previousCellPadding['B']);
424 return $previousCellPadding;
425 }
426 private function restoreCellPaddingOffset(array $previousCellPaddings) : void
427 {
428 $this->TCPDF->setCellPaddings($previousCellPaddings['L'], $previousCellPaddings['T'], $previousCellPaddings['R'], $previousCellPaddings['B']);
429 }
430 /**
431 * Gets the row height for a label. This will be the total height including wrapping
432 * but still having a maximum height
433 */
434 private function getLabelRowHeight(string $text) : float
435 {
436 $maxHeight = $this->maxRowHeight;
437 $labelHeight = $this->TCPDF->getStringHeight($this->labelCellWidth, $text);
438 $labelHeight = ceil($labelHeight * 2) / 2;
439 // round up to nearest 0.5 for stable row heights
440 if ($labelHeight > $maxHeight) {
441 return $maxHeight;
442 }
443 if ($labelHeight < $this->cellHeight) {
444 return $this->cellHeight;
445 }
446 return $labelHeight + 1;
447 }
448 /**
449 * @param false|string $url
450 * @param array<string, mixed> $rowMetadata
451 */
452 private function renderLabelLinkAndLogo($url, float $posX, float $posY, float $rowHeight, array $rowMetadata, bool $isLogoDisplayable, float &$logoWidth, float &$logoHeight) : void
453 {
454 if ($url) {
455 $this->TCPDF->Link($posX, $posY, $this->labelCellWidth, $rowHeight, $url);
456 }
457 $this->TCPDF->SetXY($posX + $this->labelCellWidth, $posY);
458 if (!$isLogoDisplayable) {
459 return;
460 }
461 if (isset($rowMetadata['logoWidth'])) {
462 $logoWidth = $rowMetadata['logoWidth'];
463 }
464 if (isset($rowMetadata['logoHeight'])) {
465 $logoHeight = $rowMetadata['logoHeight'];
466 }
467 $restoreY = $this->TCPDF->getY();
468 $restoreX = $this->TCPDF->getX();
469 $this->TCPDF->SetY($posY);
470 $this->TCPDF->SetX($posX);
471 $topMargin = 1.3;
472 // Country flags are not very high, force a bigger top margin
473 if ($logoHeight < 16) {
474 $topMargin = 2;
475 }
476 $path = Filesystem::getPathToPiwikRoot() . "/" . $rowMetadata['logo'];
477 if (file_exists($path)) {
478 $this->TCPDF->Image($path, $posX + ($leftMargin = 2), $posY + $topMargin, $logoWidth / 4);
479 }
480 $this->TCPDF->SetXY($restoreX, $restoreY);
481 }
482 /**
483 * Checks if a string might be a url or not
484 * Will return the string with an 'https' protocol if it is a valid url
485 * @return false|string
486 */
487 private function isUrl(string $value)
488 {
489 $candidate = $value;
490 if (!preg_match('~^[a-z][a-z0-9+.-]*://~i', $candidate)) {
491 $candidate = 'https://' . $candidate;
492 }
493 $host = parse_url($candidate, \PHP_URL_HOST);
494 $isValidHost = $host && strpos($host, '.') !== \false;
495 $isValidUrl = filter_var($candidate, \FILTER_VALIDATE_URL) !== \false && $isValidHost;
496 return $isValidUrl ? $candidate : \false;
497 }
498 /**
499 * This is only useful when label row is 3 lines,
500 * we needed to make max row bigger so that it can be centered vertically better
501 */
502 private function getLabelRowMaxHeight(float $rowHeight) : float
503 {
504 if ($rowHeight >= $this->maxRowHeight) {
505 return $rowHeight + $this->labelThirdLinePadding;
506 }
507 return $rowHeight;
508 }
509 /**
510 * Sets initial label width based on column count and content heuristics.
511 */
512 private function setInitialLabelWidth(int $columnsCount) : void
513 {
514 if ($columnsCount <= 1) {
515 $this->labelCellWidth = $this->totalWidth;
516 $this->cellWidth = 0;
517 return;
518 }
519 if ($columnsCount < 5) {
520 if ($columnsCount === 2) {
521 $labelRatio = $this->twoColumnLabelRatio;
522 } elseif ($columnsCount === 3) {
523 $labelRatio = $this->threeColumnLabelRatio;
524 } else {
525 $labelRatio = $this->fourColumnLabelRatio;
526 }
527 $metricColumns = $columnsCount - 1;
528 $this->labelCellWidth = max((int) round($this->totalWidth * $labelRatio), $this->minWidthLabelCellPortrait);
529 $this->cellWidth = (int) round(($this->totalWidth - $this->labelCellWidth) / $metricColumns);
530 $this->totalWidth = $this->labelCellWidth + $metricColumns * $this->cellWidth;
531 return;
532 }
533 if (!$this->reportHasData() || !$this->shouldUseShortLabelWidth()) {
534 return;
535 }
536 $this->labelCellWidth = $this->minWidthLabelCellPortraitShort;
537 $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / ($columnsCount - 1));
538 $this->totalWidth = $this->labelCellWidth + ($columnsCount - 1) * $this->cellWidth;
539 }
540 private function shrinkLabelWidthForSingleLineLabels(int $columnsCount) : void
541 {
542 if ($columnsCount <= $this->numColumnsBeforeShrink || !$this->reportHasData()) {
543 return;
544 }
545 $maxLabelWidth = $this->getMaxSingleLineLabelWidth();
546 if ($maxLabelWidth === null) {
547 return;
548 }
549 $maxLabelWidth = max($maxLabelWidth, $this->minWidthLabelCellPortraitShort);
550 if ($maxLabelWidth >= $this->labelCellWidth) {
551 return;
552 }
553 $metricColumns = $columnsCount - 1;
554 $this->labelCellWidth = $maxLabelWidth;
555 $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / $metricColumns);
556 $this->totalWidth = $this->labelCellWidth + $metricColumns * $this->cellWidth;
557 }
558 private function capLabelWidthForManyColumns(int $columnsCount) : void
559 {
560 if ($columnsCount <= $this->numColumnsBeforeShrink) {
561 return;
562 }
563 $maxLabelWidth = round($this->totalWidth * 0.35, 2);
564 if ($this->labelCellWidth <= $maxLabelWidth) {
565 return;
566 }
567 $metricColumns = $columnsCount - 1;
568 $this->labelCellWidth = $maxLabelWidth;
569 $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / $metricColumns);
570 $this->totalWidth = $this->labelCellWidth + $metricColumns * $this->cellWidth;
571 }
572 private function getMaxSingleLineLabelWidth() : ?float
573 {
574 if (!empty($this->tableWidthCache['maxSingleLineLabelWidthFor']) && (float) $this->tableWidthCache['maxSingleLineLabelWidthFor'] === (float) $this->labelCellWidth) {
575 return $this->tableWidthCache['maxSingleLineLabelWidth'];
576 }
577 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
578 $rowsMetadata = array();
579 if (!empty($this->reportRowsMetadata)) {
580 $rowsMetadata = $this->reportRowsMetadata->getRows();
581 }
582 $maxWidth = 0.0;
583 foreach ($this->report->getRows() as $rowId => $row) {
584 $label = $row->getColumn('label');
585 if ($label === \false || $label === null) {
586 continue;
587 }
588 $labelText = $this->buildFormattedLabelTextForRow($rowId, (string) $label, $rowsMetadata, $this->maxLabelCharacter);
589 if ($this->TCPDF->getNumLines($labelText, $this->labelCellWidth) > 1) {
590 return null;
591 }
592 $width = $this->TCPDF->GetStringWidth($labelText);
593 if ($width > $maxWidth) {
594 $maxWidth = $width;
595 }
596 }
597 if ($maxWidth <= 0) {
598 $this->tableWidthCache['maxSingleLineLabelWidth'] = null;
599 $this->tableWidthCache['maxSingleLineLabelWidthFor'] = $this->labelCellWidth;
600 return null;
601 }
602 $padding = $this->TCPDF->getCellPaddings();
603 $maxWidth = $maxWidth + $padding['L'] + $padding['R'];
604 $this->tableWidthCache['maxSingleLineLabelWidth'] = $maxWidth;
605 $this->tableWidthCache['maxSingleLineLabelWidthFor'] = $this->labelCellWidth;
606 return $maxWidth;
607 }
608 private function getColumnWidth(string $columnId) : float
609 {
610 if (isset($this->columnCellWidths[$columnId])) {
611 return (float) $this->columnCellWidths[$columnId];
612 }
613 if ($columnId === 'label') {
614 return (float) $this->labelCellWidth;
615 }
616 return (float) $this->cellWidth;
617 }
618 /**
619 * This function will try to show all values for selected metric columns.
620 * Will adjust other column widths to accommodate this
621 */
622 private function adjustMetricColumnWidthsToContent() : void
623 {
624 if (!$this->reportHasData()) {
625 return;
626 }
627 $metricColumnsToAdjust = array('revenue', 'ecommerce_revenue', 'avg_time_on_site');
628 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
629 foreach ($metricColumnsToAdjust as $columnId) {
630 $this->adjustMetricColumnWidthToContent($columnId);
631 }
632 }
633 /**
634 * This function will try to adjust column width based on the content.
635 * This will try to make other columns smaller to accommodate this
636 */
637 private function adjustMetricColumnWidthToContent(string $columnId) : void
638 {
639 if (!array_key_exists($columnId, $this->reportColumns) || !isset($this->columnCellWidths[$columnId])) {
640 return;
641 }
642 $requiredWidth = $this->getMaxFormattedColumnWidth($columnId);
643 if ($requiredWidth <= 0) {
644 return;
645 }
646 $currentWidth = $this->columnCellWidths[$columnId];
647 if ($requiredWidth <= $currentWidth) {
648 return;
649 }
650 $additionalWidth = $requiredWidth - $currentWidth;
651 if ($additionalWidth <= 1.0) {
652 $requiredWidth += $this->expandedMetricRightPadding;
653 $additionalWidth = $requiredWidth - $currentWidth;
654 }
655 $remainingWidthToGain = $additionalWidth;
656 $minMetricWidth = 10;
657 $adjustableColumns = array();
658 foreach ($this->columnCellWidths as $otherColumnId => $width) {
659 if ($otherColumnId === 'label' || $otherColumnId === $columnId) {
660 continue;
661 }
662 if ($width <= $minMetricWidth) {
663 continue;
664 }
665 $adjustableColumns[$otherColumnId] = $width;
666 }
667 while ($remainingWidthToGain > 0 && !empty($adjustableColumns)) {
668 $share = $remainingWidthToGain / count($adjustableColumns);
669 $updatedColumns = array();
670 foreach ($adjustableColumns as $otherColumnId => $availableWidth) {
671 $maxReducible = $availableWidth - $minMetricWidth;
672 if ($maxReducible <= 0) {
673 continue;
674 }
675 $reduction = min($maxReducible, $share);
676 if ($reduction <= 0) {
677 continue;
678 }
679 $this->columnCellWidths[$otherColumnId] -= $reduction;
680 $remainingWidthToGain -= $reduction;
681 $newWidth = $availableWidth - $reduction;
682 if ($newWidth > $minMetricWidth && $remainingWidthToGain > 0) {
683 $updatedColumns[$otherColumnId] = $newWidth;
684 }
685 if ($remainingWidthToGain <= 0) {
686 break 2;
687 }
688 }
689 $adjustableColumns = $updatedColumns;
690 }
691 $appliedWidth = $additionalWidth - $remainingWidthToGain;
692 if ($appliedWidth <= 0) {
693 return;
694 }
695 $this->columnCellWidths[$columnId] += $appliedWidth;
696 }
697 /**
698 * Computes maximum column width for a given metric column
699 */
700 private function getMaxFormattedColumnWidth(string $columnId) : float
701 {
702 if (!empty($this->tableWidthCache) && isset($this->tableWidthCache['metricMaxWidths'][$columnId])) {
703 $maxWidth = $this->tableWidthCache['metricMaxWidths'][$columnId];
704 if ($maxWidth <= 0) {
705 return 0;
706 }
707 return $maxWidth + 2;
708 }
709 $maxWidth = 0;
710 foreach ($this->report->getRows() as $row) {
711 $value = $row->getColumn($columnId);
712 if ($value === \false || $value === null) {
713 continue;
714 }
715 $formattedValue = NumberFormatter::getInstance()->format($value);
716 $width = $this->TCPDF->GetStringWidth($formattedValue);
717 if ($width > $maxWidth) {
718 $maxWidth = $width;
719 }
720 }
721 if ($maxWidth <= 0) {
722 return 0;
723 }
724 return $maxWidth + 2;
725 }
726 private function initializeTableWidthCache() : void
727 {
728 $this->tableWidthCache = array('ready' => \true, 'labelTooLong' => \false, 'labelFitsShortWidth' => \true, 'maxLabelLength' => 0, 'metricMaxWidths' => array(), 'maxSingleLineLabelWidth' => null, 'maxSingleLineLabelWidthFor' => null);
729 if (!$this->reportHasData()) {
730 return;
731 }
732 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
733 $maxCharacters = $this->maxLabelCharacter;
734 $rowsMetadata = array();
735 if (!empty($this->reportRowsMetadata)) {
736 $rowsMetadata = $this->reportRowsMetadata->getRows();
737 }
738 $metricColumnsToAdjust = array('revenue', 'ecommerce_revenue', 'avg_time_on_site');
739 foreach ($metricColumnsToAdjust as $columnId) {
740 $this->tableWidthCache['metricMaxWidths'][$columnId] = 0.0;
741 }
742 foreach ($this->report->getRows() as $rowId => $row) {
743 $label = $row->getColumn('label');
744 if ($label !== \false && $label !== null) {
745 $rawLabel = (string) $label;
746 $length = mb_strlen($rawLabel);
747 if ($length > $this->tableWidthCache['maxLabelLength']) {
748 $this->tableWidthCache['maxLabelLength'] = $length;
749 }
750 if ($length > $maxCharacters) {
751 $this->tableWidthCache['labelTooLong'] = \true;
752 }
753 if ($this->tableWidthCache['labelFitsShortWidth']) {
754 $formattedLabel = $this->buildFormattedLabelTextForRow($rowId, $rawLabel, $rowsMetadata, $maxCharacters);
755 if ($this->TCPDF->getNumLines($formattedLabel, $this->minWidthLabelCellPortraitShort) > 1) {
756 $this->tableWidthCache['labelFitsShortWidth'] = \false;
757 }
758 }
759 }
760 foreach ($metricColumnsToAdjust as $columnId) {
761 if (!array_key_exists($columnId, $this->reportColumns)) {
762 continue;
763 }
764 $value = $row->getColumn($columnId);
765 if ($value === \false || $value === null) {
766 continue;
767 }
768 $formattedValue = NumberFormatter::getInstance()->format($value);
769 $width = $this->TCPDF->GetStringWidth($formattedValue);
770 if ($width > $this->tableWidthCache['metricMaxWidths'][$columnId]) {
771 $this->tableWidthCache['metricMaxWidths'][$columnId] = $width;
772 }
773 }
774 }
775 }
776 /**
777 * Will check if label column could use a shorter width.
778 * This is done so that we can fit more metrics in the same row for data table with no label that is too long
779 */
780 private function shouldUseShortLabelWidth() : bool
781 {
782 if (empty($this->tableWidthCache) || empty($this->tableWidthCache['ready'])) {
783 $this->initializeTableWidthCache();
784 }
785 if (empty($this->tableWidthCache['maxLabelLength'])) {
786 return \false;
787 }
788 if (!empty($this->tableWidthCache['labelTooLong'])) {
789 return \false;
790 }
791 if ($this->tableWidthCache['maxLabelLength'] >= $this->labelShortContentThreshold) {
792 return \false;
793 }
794 if (empty($this->tableWidthCache['labelFitsShortWidth'])) {
795 return \false;
796 }
797 return $this->tableWidthCache['maxLabelLength'] > 0;
798 }
799 private function buildFormattedLabelTextForRow(int $rowId, string $label, array $rowsMetadata, int $maxCharacters) : string
800 {
801 $labelText = trim($label);
802 $labelText = $this->limitTextLength($labelText, $maxCharacters);
803 if (isset($rowsMetadata[$rowId])) {
804 $rowMeta = $rowsMetadata[$rowId]->getColumns();
805 if (isset($rowMeta['logo'])) {
806 $labelText = str_repeat(' ', $this->leftSpacesBeforeLogo) . $labelText;
807 }
808 }
809 return $this->formatText($labelText);
810 }
811 private function paintGraph() : void
812 {
813 $imageGraph = parent::getStaticGraph($this->reportMetadata, self::IMAGE_GRAPH_WIDTH_PORTRAIT, self::IMAGE_GRAPH_HEIGHT, $this->evolutionGraph, $this->segment);
814 $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());
815 unset($imageGraph);
816 }
817 /**
818 * Draw the table header (first row)
819 */
820 private function paintReportTableHeader() : void
821 {
822 $initPosX = 10;
823 $this->initializeTableColumnWidths();
824 $this->setupHeaderRenderingStyle();
825 $posY = $this->TCPDF->GetY();
826 list($columnData, $maxCellHeight) = $this->buildHeaderColumnData();
827 $this->TCPDF->SetXY($initPosX, $posY);
828 $this->renderHeaderColumns($columnData, $initPosX, $posY, $maxCellHeight);
829 $extraSpacing = 1;
830 $this->TCPDF->Ln($extraSpacing);
831 $this->TCPDF->SetXY($initPosX, $posY + $maxCellHeight + $extraSpacing);
832 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
833 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
834 }
835 /**
836 * Will initialize table column widths,
837 * this will include adjusting label and revenue columns
838 */
839 private function initializeTableColumnWidths() : void
840 {
841 $columnsCount = count($this->reportColumns);
842 if ($columnsCount === 0) {
843 return;
844 }
845 $this->totalWidth = $this->reportWidthPortrait;
846 $minLabelWidth = $this->minWidthLabelCellPortrait;
847 $this->labelCellWidth = max(round($this->totalWidth / $columnsCount), $minLabelWidth);
848 $metricColumns = max(1, $columnsCount - 1);
849 $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / $metricColumns);
850 $this->totalWidth = $this->labelCellWidth + $metricColumns * $this->cellWidth;
851 $this->initializeTableWidthCache();
852 $this->setInitialLabelWidth($columnsCount);
853 $this->capLabelWidthForManyColumns($columnsCount);
854 $this->shrinkLabelWidthForSingleLineLabels($columnsCount);
855 $this->columnCellWidths = array();
856 foreach ($this->reportColumns as $columnId => $_) {
857 $this->columnCellWidths[$columnId] = $columnId === 'label' ? $this->labelCellWidth : $this->cellWidth;
858 }
859 $this->adjustMetricColumnWidthsToContent();
860 $this->totalWidth = array_sum($this->columnCellWidths);
861 }
862 private function setupHeaderRenderingStyle() : void
863 {
864 $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]);
865 $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]);
866 $this->TCPDF->SetLineWidth(0.3);
867 $this->setBorderColor();
868 $this->TCPDF->SetFont($this->reportFont, 'B');
869 $this->TCPDF->SetFillColor(255);
870 $this->TCPDF->SetTextColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]);
871 $this->TCPDF->SetDrawColor(255);
872 }
873 /**
874 * Will adjust table headers based on their column name and make them be closer to the table
875 * @return array
876 */
877 private function buildHeaderColumnData() : array
878 {
879 $columnData = array();
880 $maxCellHeight = $this->cellHeight;
881 foreach ($this->reportColumns as $columnId => $columnName) {
882 $columnName = $this->formatText($columnName);
883 $columnWidth = $this->getColumnWidth($columnId);
884 $textHeight = $this->TCPDF->getStringHeight($columnWidth, $columnName);
885 $cellHeight = max($textHeight, $this->cellHeight);
886 $columnData[] = array('text' => $columnName, 'width' => $columnWidth, 'height' => $cellHeight, 'textHeight' => $textHeight);
887 if ($cellHeight > $maxCellHeight) {
888 $maxCellHeight = $cellHeight;
889 }
890 }
891 return array($columnData, $maxCellHeight);
892 }
893 private function renderHeaderColumns(array $columnData, float $initPosX, float $posY, float $maxCellHeight) : void
894 {
895 $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]);
896 $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]);
897 $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]);
898 $posX = $initPosX;
899 foreach ($columnData as $columnInfo) {
900 $columnWidth = $columnInfo['width'];
901 $textHeight = $columnInfo['textHeight'];
902 $this->TCPDF->Rect($posX, $posY, $columnWidth, $maxCellHeight, 'F');
903 $textPosY = $this->calculateHeaderTextY($posY, $maxCellHeight, $textHeight);
904 $this->TCPDF->SetXY($posX, $textPosY);
905 $this->TCPDF->MultiCell($columnWidth, $this->cellHeight, $columnInfo['text'], 0, 'L', \false, 0, '', '', \true, 0, \false, \true, 0, 'T');
906 $this->TCPDF->SetXY($posX + $columnWidth, $posY);
907 $posX = $this->TCPDF->GetX();
908 }
909 }
910 private function calculateHeaderTextY(float $posY, float $maxCellHeight, float $textHeight) : float
911 {
912 if ($textHeight <= 0) {
913 $textHeight = $this->cellHeight;
914 }
915 $bottomPadding = $this->headerBottomPadding;
916 if ($textHeight <= $this->cellHeight) {
917 $bottomPadding = 0;
918 } elseif ($textHeight <= $this->cellHeight * 2) {
919 $bottomPadding = $this->headerBottomPaddingShort;
920 }
921 $availableSpace = max(0, $maxCellHeight - $textHeight);
922 $textPosY = $posY + $availableSpace - $bottomPadding;
923 $minY = $posY;
924 $maxY = $posY + $maxCellHeight - $textHeight;
925 if ($textPosY < $minY) {
926 return $minY;
927 }
928 if ($textPosY > $maxY) {
929 return $maxY;
930 }
931 return $textPosY;
932 }
933 /**
934 * Prints a message
935 *
936 * @param string $message
937 */
938 private function paintMessage($message) : void
939 {
940 $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
941 $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
942 $message = $this->formatText($message);
943 $this->TCPDF->Write("1em", $message);
944 $this->TCPDF->Ln();
945 }
946 /**
947 * PDF reports embed graphs directly, so there are never any separate attachments.
948 *
949 * @param $report
950 * @param $processedReports
951 * @param $prettyDate
952 * @return array Always empty.
953 */
954 public function getAttachments($report, $processedReports, $prettyDate) : array
955 {
956 return array();
957 }
958 }
959