]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/IO/Pdf.php
Rework PDF footer so any height will be ok
[galette.git] / galette / lib / Galette / IO / Pdf.php
1 <?php
2
3 /**
4 * Copyright © 2003-2024 The Galette Team
5 *
6 * This file is part of Galette (https://galette.eu).
7 *
8 * Galette is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * Galette is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 namespace Galette\IO;
23
24 use Galette\Core\I18n;
25 use Galette\Core\Preferences;
26 use Galette\Entity\PdfModel;
27 use Analog\Analog;
28 use Slim\Routing\RouteParser;
29 use TCPDF;
30
31 /*
32 * TCPDF configuration file for Galette
33 */
34 require_once GALETTE_CONFIG_PATH . 'galette_tcpdf_config.php';
35
36 /**
37 * PDF class for galette
38 *
39 * @author John Perr <johnperr@abul.org>
40 * @author Johan Cwiklinski <johan@x-tnd.be>
41 */
42
43 class Pdf extends TCPDF
44 {
45 public const FONT = 'DejaVuSans';
46 public const FONT_SIZE = 10;
47
48 protected Preferences $preferences;
49 protected I18n $i18n;
50 private PdfModel $model;
51 private bool $paginated = false;
52 protected string $filename;
53 private bool $has_footer = true;
54 protected float $footer_height;
55
56 /**
57 * Main constructor, set creator and author
58 *
59 * @param Preferences $prefs Preferences
60 * @param ?PdfModel $model Related model
61 */
62 public function __construct(Preferences $prefs, ?PdfModel $model = null)
63 {
64 global $i18n;
65
66 $this->preferences = $prefs;
67 $this->i18n = $i18n;
68 parent::__construct('P', 'mm', 'A4', true, 'UTF-8');
69 //set some values
70 $this->SetCreator(PDF_CREATOR);
71 //add helvetica, hard-called from lib
72 $this->SetFont('helvetica');
73 //and then, set real font
74 $this->SetFont(self::FONT, '', self::FONT_SIZE);
75 $name = preg_replace(
76 '/%s/',
77 $this->preferences->pref_nom,
78 _T("Association %s")
79 );
80 $this->SetAuthor(
81 $name . ' (using Galette ' . GALETTE_VERSION . ')'
82 );
83
84 if ($this->i18n->isRTL()) {
85 $this->setRTL(true);
86 }
87
88 if ($model !== null) {
89 $this->model = $model;
90 $this->SetTitle($this->model->htitle);
91 }
92
93 $this->init();
94 if ($this->has_footer) {
95 $this->calculateFooterHeight();
96 }
97 }
98
99 /**
100 * Initialize PDF
101 *
102 * @return void
103 */
104 public function init(): void
105 {
106 $this->Open();
107 $this->AddPage();
108 }
109
110 /**
111 * No header
112 *
113 * @return void
114 */
115 protected function setNoHeader(): void
116 {
117 $this->SetPrintHeader(false);
118 $this->setHeaderMargin(0);
119 }
120
121 /**
122 * No footer
123 *
124 * @return void
125 */
126 protected function setNoFooter(): void
127 {
128 $this->SetPrintFooter(false);
129 $this->setFooterMargin(0);
130 $this->has_footer = false;
131 }
132
133 /**
134 * Calculate footer height
135 *
136 * @return void
137 */
138 private function calculateFooterHeight(): void
139 {
140 $pdf = clone $this;
141 $y_orig = $pdf->getY();
142 $this->Footer($pdf);
143 $y_end = $pdf->getY();
144 $this->footer_height = $y_end - $y_orig;
145 }
146
147 /**
148 * Set show pagination
149 *
150 * @return void
151 */
152 public function showPagination(): void
153 {
154 $this->paginated = true;
155 }
156
157 /**
158 * Destructor
159 */
160 public function __destruct()
161 {
162 parent::__destruct();
163 }
164
165 /**
166 * This method is automatically called in case of fatal error;
167 * it simply outputs the message and halts the execution.
168 * An inherited class may override it to customize the error
169 * handling but should always halt the script, or the resulting
170 * document would probably be invalid.
171 * 2004-06-11 :: Nicola Asuni : changed bold tag with strong
172 * 2007-07-21 :: John Perr : changed function to return error to session
173 * 2017-02-14 :: Johan Cwiklinski : use slim's flash message; do not rely on session for redirect
174 *
175 * @param string $msg The error message
176 *
177 * @return void
178 * @access public
179 * @since 1.0
180 */
181 public function Error(mixed $msg): void // phpcs:ignore PSR1.Methods.CamelCapsMethodName
182 {
183 global $container;
184
185 Analog::log(
186 'PDF error: ' . $msg,
187 Analog::ERROR
188 );
189
190 $container->get('flash')->addMessage(
191 'error_detected',
192 $msg
193 );
194
195 $redirect = (isset($_SERVER['HTTP_REFERER']) ?
196 $_SERVER['HTTP_REFERER'] : $container->get(RouteParser::class)->urlFor('slash'));
197 header('Location: ' . $redirect);
198 die();
199 }
200
201 /**
202 * Converts color from HTML format #RRVVBB
203 * to RGB 3 colors array.
204 *
205 * @param string $hex6 7 chars string #RRVVBB
206 *
207 * @return array<string,float|int>
208 */
209 public function colorHex2Dec(string $hex6): array
210 {
211 $dec = array(
212 "R" => hexdec(substr($hex6, 1, 2)),
213 "G" => hexdec(substr($hex6, 3, 2)),
214 "B" => hexdec(substr($hex6, 5, 2))
215 );
216 return $dec;
217 }
218
219 /**
220 * Draws PDF page Header
221 *
222 * @return void
223 */
224 public function Header(): void // phpcs:ignore PSR1.Methods.CamelCapsMethodName
225 {
226 //just override default header to prevent black line at top
227 }
228
229 /**
230 * Draws PDF page footer
231 *
232 * @param ?TCPDF $pdf PDF instance
233 *
234 * @return void
235 */
236 public function Footer(TCPDF $pdf = null): void // phpcs:ignore PSR1.Methods.CamelCapsMethodName
237 {
238 if ($pdf === null) {
239 $pdf = $this;
240 $pdf->SetY(-($this->footer_height + 15));
241 }
242 if (isset($this->model)) {
243 $hfooter = '';
244 if (trim($this->model->hstyles) !== '') {
245 $hfooter .= "<style>\n" . $this->model->hstyles . "\n</style>\n\n";
246 }
247 $hfooter .= $this->model->hfooter;
248 $pdf->writeHtml($hfooter);
249 } else {
250 $address = $this->preferences->getPostalAddress();
251 $hfooter = '<style>div#pdf_footer {text-align: center;font-size: 0.7em;}</style>';
252 $hfooter .= '<div id="pdf_footer">' . nl2br($address) . '</div>';
253 $pdf->writeHTML($hfooter);
254 }
255
256 if ($this->paginated) {
257 $pdf->SetFont(self::FONT, '', self::FONT_SIZE - 3);
258 $pdf->Ln();
259 $pdf->Cell(
260 0,
261 4,
262 $this->getAliasRightShift() . $this->PageNo() .
263 '/' . $this->getAliasNbPages(),
264 0,
265 1,
266 ($this->i18n->isRTL() ? 'L' : 'R')
267 );
268 }
269 }
270
271 /**
272 * Draws PDF page header
273 *
274 * @param ?string $title Additional title to display just after logo
275 *
276 * @return void
277 */
278 public function PageHeader(string $title = null): void // phpcs:ignore PSR1.Methods.CamelCapsMethodName
279 {
280 if (isset($this->model)) {
281 $this->modelPageHeader($title);
282 } else {
283 $this->standardPageHeader($title);
284 }
285 }
286
287 /**
288 * Draws models PDF page header
289 *
290 * @param ?string $title Additional title to display just after logo
291 *
292 * @return void
293 */
294 protected function modelPageHeader(string $title = null): void
295 {
296 $html = null;
297 if (trim($this->model->hstyles) !== '') {
298 $html .= "<style>\n" . $this->model->hstyles . "\n</style>\n\n";
299 }
300 $html .= "<div dir=\"" . ($this->i18n->isRTL() ? 'rtl' : 'ltr') . "\">" . $this->model->hheader . "</div>";
301 $this->writeHtml($html, true, false, true, false, '');
302
303 if ($title !== null) {
304 $this->writeHtml('<h2 style="text-align:center;">' . $title . '</h2>');
305 }
306
307 if (trim($this->model->title) !== '') {
308 $htitle = '';
309 if (trim($this->model->hstyles) !== '') {
310 $htitle .= "<style>\n" . $this->model->hstyles .
311 "\n</style>\n\n";
312 }
313 $htitle .= '<div id="pdf_title">' . $this->model->htitle . '</div>';
314 $this->writeHtml($htitle);
315 }
316 if (trim($this->model->subtitle) !== '') {
317 $hsubtitle = '';
318 if (trim($this->model->hstyles) !== '') {
319 $hsubtitle .= "<style>\n" . $this->model->hstyles .
320 "\n</style>\n\n";
321 }
322 $hsubtitle .= '<div id="pdf_subtitle">' . $this->model->hsubtitle .
323 '</div>';
324 $this->writeHtml($hsubtitle);
325 }
326 if (
327 trim($this->model->title) !== ''
328 || trim($this->model->subtitle) !== ''
329 ) {
330 $this->Ln(5);
331 }
332 }
333
334 /**
335 * Draws standard PDF page header
336 *
337 * @param ?string $title Additional title to display just after logo
338 *
339 * @return void
340 */
341 protected function standardPageHeader(string $title = null): void
342 {
343 //default header
344 $print_logo = new \Galette\Core\PrintLogo();
345 $logofile = $print_logo->getPath();
346
347 // Set logo size to max width 30 mm or max height 25 mm
348 $ratio = $print_logo->getWidth() / $print_logo->getHeight();
349 if ($ratio < 1) {
350 if ($print_logo->getHeight() > 16) {
351 $hlogo = 25;
352 } else {
353 $hlogo = $print_logo->getHeight();
354 }
355 $wlogo = round($hlogo * $ratio);
356 } else {
357 if ($print_logo->getWidth() > 16) {
358 $wlogo = 30;
359 } else {
360 $wlogo = $print_logo->getWidth();
361 }
362 $hlogo = round($wlogo / $ratio);
363 }
364
365 $this->SetFont(self::FONT, 'B', self::FONT_SIZE + 4);
366 $this->SetTextColor(0, 0, 0);
367
368 $y = $this->GetY();
369 $this->Ln(2);
370 $ystart = $this->GetY();
371
372 $this->MultiCell(
373 180 - $wlogo,
374 6,
375 $this->preferences->pref_nom,
376 0,
377 ($this->i18n->isRTL() ? 'R' : 'L')
378 );
379 $this->SetFont(self::FONT, 'B', self::FONT_SIZE + 2);
380
381 if ($title !== null) {
382 $this->Cell(0, 6, $title, 0, 1, ($this->i18n->isRTL() ? 'R' : 'L'), false);
383 }
384 $yend = $this->getY(); //store position at the end of the text
385
386 $this->SetY($ystart);
387 if ($this->i18n->isRTL()) {
388 $x = $this->getX();
389 } else {
390 $x = 190 - $wlogo; //right align
391 }
392 $this->Image($logofile, $x, $this->GetY(), $wlogo, $hlogo);
393 $this->y += $hlogo + 3;
394 //if position after logo is < than position after text,
395 //we have to change y
396 if ($this->getY() < $yend) {
397 $this->setY($yend);
398 }
399 }
400
401 /**
402 * Draws body from model
403 *
404 * @return void
405 */
406 public function PageBody(): void // phpcs:ignore PSR1.Methods.CamelCapsMethodName
407 {
408 $hbody = '';
409 if (trim($this->model->hstyles) !== '') {
410 $hbody .= "<style>\n" . $this->model->hstyles . "\n</style>\n\n";
411 }
412 $hbody .= $this->model->hbody;
413 $this->writeHtml($hbody);
414 }
415
416 /**
417 * Fix text size
418 *
419 * @param string $text Text content
420 * @param integer $maxsize Maximal size
421 * @param integer $fontsize Font size
422 * @param string $fontstyle Font style (defaults to '')
423 * @param ?string $fontname Font name (defaults to static::FONT)
424 *
425 * @return void
426 */
427 protected function fixSize(
428 string $text,
429 int $maxsize,
430 int $fontsize,
431 string $fontstyle = '',
432 string $fontname = null
433 ): void {
434 if ($fontname === null) {
435 $fontname = static::FONT;
436 }
437 $this->SetFontSize($fontsize);
438 while ((int)$this->GetStringWidth($text, $fontname, $fontstyle, $fontsize) > $maxsize) {
439 $fontsize--;
440 $this->SetFontSize($fontsize);
441 }
442 }
443
444 /**
445 * Cut a string
446 *
447 * @param string $str Original string
448 * @param integer $length Max length
449 *
450 * @return string
451 */
452 protected function cut(string $str, int $length): string
453 {
454 $length = $length - 2; //keep a margin
455 if ((int)$this->GetStringWidth($str) > $length) {
456 while ((int)$this->GetStringWidth($str . '...') > $length) {
457 $str = mb_substr($str, 0, -1, 'UTF-8');
458 }
459 $str .= '...';
460 }
461 return $str;
462 }
463
464 /**
465 * Stretch a header string
466 *
467 * @param string $str Original string
468 * @param integer $length Max length
469 *
470 * @return string
471 */
472 protected function stretchHead(string $str, int $length): string
473 {
474 $this->SetFont(self::FONT, 'B', self::FONT_SIZE);
475 $stretch = 100;
476 if ((int)$this->GetStringWidth($str) > $length) {
477 while ((int)$this->GetStringWidth($str) > $length) {
478 $this->setFontStretching(--$stretch);
479 }
480 }
481 return $str;
482 }
483
484 /**
485 * Get filename
486 *
487 * @return string
488 */
489 public function getFilename(): string
490 {
491 return $this->filename;
492 }
493
494 /**
495 * Download PDF from browser
496 *
497 * @return string
498 */
499 public function download(): string
500 {
501 return $this->Output($this->filename, 'D');
502 }
503 }