Do generowania dokumentów PDF w PHP wykorzystuję popularną klasę TCPDF. Niestety ma ona szereg przeszkód, jakie musimy pokonać, aby uzyskać poprawnie działające i wyglądające czcionki z ogonkami. Naturalnie TCPDF obsługę kodowania UTF-8, ale nie jest ona do końca idealna. Przede wszystkim musimy posiadać czcionki Unicode, które osadzone w dokumencie PDF znacząco zwiększają jego rozmiar. Pewne rozwiązanie problemu opublikował Karol Nowacki na swoim blogu, jednak nie do końca trafia ono w sedno problemu.
Od wersji TCPDF 5.2 pojawiła się możliwość osadzania podzbioru znaków (subsetting) wykorzystanej czcionki. Chodzi o to, że do dokumentu PDF dołączane są jedynie te znaki, które wykorzystaliśmy przy jego tworzeniu, ograniczając rozmiar pliku wynikowego. Rozwiązanie dobre, ale nie zawsze.
W przypadku zastosowania owego podzbioru, ograniczamy możliwość edycji pliku PDF osobom, które nie posiadają w systemie wykorzystanej przez nas czcionki. Po drugie, czas generowania dokumentu z wykorzystaniem subsettingu jest koszmarnie długi, dodatkowo uzależniony od ilości tekstu.
Rozmiar pliku czy szybkość generowania?
Do testów wykorzystamy fragment Pana Tadeusza, zapisanego w UTF-8 oraz czcionki DejaVuSans Unicode.
- "Litwo! Ojczyzno moja! Ty jesteś jak zdrowie. Ile cię stracił. Dziś człowieka rodu, obyczajów! Dość, że niecierpliwa młodzież nieraz jego wiernym ludem! Jak ów Wespazyjanus nie odmówi. To jedno puste miejsce, jak pieniądze Żydzi. To mówiąc spojrzał zyzem, gdzie mieszkał, dzieckiem będąc, przed młodzieżą o autorów pytała Tadeusza wsparła się tajemnie, Ścigany od baśni historyje gadał."
Przygotowana czcionka (dejavusans.z) dla TCPDF waży 339kB. Tak jak wspomniałem wcześniej, możemy osadzić ja całkowicie lub tylko jej podzbiór. Na początek osadzimy ją całkowicie :
- <?php
- $pdf = new TCPDFPL(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8');
- $pdf->setFont('dejavusans', '', 10, '', false); // osadzamy całkowicie
- $pdf->addPage();
- $pdf->writeHTMLCell(100, 100, 10,50, $utf8);
- $pdf->Output('document.pdf', 'D');
- ?>
Dokument PDF generował się około 100ms, osiągając rozmiar 372kB. Zobaczmy jak to będzie wyglądało w przypadku osadzenia podzbioru czcionki :
- <?php
- $pdf = new TCPDFPL(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8');
- $pdf->setFont('dejavusans', '', 10, '', true); // osadzamy podzbiór
- $pdf->addPage();
- $pdf->writeHTMLCell(100, 100, 10,50, $utf8);
- $pdf->Output('document.pdf', 'D');
- ?>
Tym razem dokument osiągnął rozmiar 50kB, jednak generował się ponad 5 sekund. Czas potrzebny na wygenerowanie dokumentu wzrasta zarówno w przypadku zwiększenia ilości tekstu jak i ilości wykorzystanych czcionek. Mamy zatem do wyboru mały rozmiar dokumentu, albo przyzwoity czas generowania pliku. Niestety - oba rozwiązania wciąż wymagają posiadania czcionki Unicode. Co więc w przypadku tradycyjnych czcionek TrueType (Arial, Helvetica, Times itp.) ?
Odchudzanie czcionek Unicode i nie tylko
Tutaj pojawia się pomysł Karola Nowackiego - możemy taką czcionkę odpowiednio odchudzić stosując jedynie mapę kodową ISO-8859-2. Zarówno dla czcionek TrueType Unicode jak i TrueType. Problem w tym, że TCPDF (jak słusznie zauważył autor) nie radzi sobie z tym kodowaniem i trzeba zrobić mały "myk".
Na początek postępujemy identycznie :
1. Tworzymy odchudzoną czcionkę dla TCPDF
ttf2ufm -b -L iso-8859-2.map DejaVuSans.ttf dejavusans
php -q makefont.php dejavusans.pfg dejavusans.afm iso-8859-2
2. Ponieważ polskie znaki nie różnią się szerokością od wersji bezogonkowych, dopisujemy ich szerokości wykorzystując szerokości ich odpowiedników do pliku dejavusans.php
- <?php
- $cw[260] = $cw[65]; // Ą = A
- $cw[261] = $cw[97]; // ą = a
- $cw[262] = $cw[67]; // Ć = C
- $cw[263] = $cw[99]; // ć = c
- $cw[280] = $cw[69]; // Ę = E
- $cw[281] = $cw[101]; // ę = e
- $cw[321] = $cw[76]; // Ł = L
- $cw[322] = $cw[108]; // Ł = l
- $cw[323] = $cw[78]; // Ń = N
- $cw[324] = $cw[110]; // ń = n
- $cw[211] = $cw[79]; // Ó = O
- $cw[243] = $cw[111]; // ó = o
- $cw[346] = $cw[83]; // Ś = S
- $cw[347] = $cw[115]; // ś = s
- $cw[377] = $cw[90]; // Ż = Z
- $cw[378] = $cw[122]; // ż = z
- $cw[379] = $cw[90]; // Ź = Z
- $cw[380] = $cw[122]; // ź = z
- ?>
3. Tworzymy klasę dziedziczącą TCPDF, nadpisując jedną z metod
- <?php
- class TCPDFPL extends TCPDF
- {
- protected function UTF8ToLatin1($str)
- {
- if (!$this->isunicode)
- {
- return $str;
- }
- }
- }
- ?>
4. Testujemy
- <?php
- $pdf = new TCPDFPL(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8');
- $pdf->setFont('dejavusans', '', 10, '', false); // osadzamy całkowicie
- $pdf->addPage();
- $pdf->writeHTMLCell(100, 100, 10,50, $utf8);
- $pdf->Output('document.pdf', 'D');
- ?>
Tym razem przygotowana czcionka (dejvausans.z) zajmuje tylko 51kB. Bez problemu osadziliśmy ją całkowicie w dokumencie PDF przy czasie generowania zaledwie 60ms.
Wszystko pięknie, ale ...
Metoda Karola działa prawie prawidłowo, rozmiar dokumentu jest sporo mniejszy przy zachowaniu szybkości generowania PDF-a. Jednak dodane szerokości polski znaków nie zostały uwzględnione, co skutkuje niewłaściwym justowaniem tekstu lub wyrównaniem do prawego marginesu.

Zaradzić temu możemy poprzez nadpisanie jeszcze jedenej metody w klasie TCPDFPL :
- <?php
- class TCPDFPL extends TCPDF
- {
- protected function UTF8ArrToLatin1($unicode)
- {
- if ((!$this->isunicode) || $this->isUnicodeFont())
- {
- return $unicode;
- }
- foreach ($unicode as $char)
- {
- {
- $outarr[] = $char;
- } else
- {
- $outarr[] = $this->unicode->uni_utf8tolatin[$char];
- } elseif ($char == 0xFFFD)
- {
- } else
- {
- $outarr[] = 63;
- }
- }
- return $outarr;
- }
- protected function UTF8ToLatin1($str)
- {
- if (!$this->isunicode)
- {
- return $str;
- }
- }
- }
- ?>
Po tym zabiegu, justowanie i pozycjonowanie tekstu znów działa prawidłowo.

Alternatywne rozwiązanie
W przypadku gdy nie chcemy bawić się w dopisywanie definicji szerokości dla polskich znaków w każdej czcionce, możemy użyć jeszcze jednej modyfikacji :
- <?php
- class TCPDFPL extends TCPDF
- {
- protected function unicode2ascii($unicode)
- {
- switch ($unicode)
- {
- case 260: return 65;
- case 261: return 97;
- case 262: return 67;
- case 263: return 99;
- case 280: return 69;
- case 281: return 101;
- case 321: return 76;
- case 322: return 108;
- case 323: return 78;
- case 324: return 110;
- case 211: return 79;
- case 243: return 111;
- case 346: return 83;
- case 347: return 115;
- case 377: return 90;
- case 378: return 122;
- case 379: return 90;
- case 380: return 122;
- default: return $unicode;
- }
- }
- protected function UTF8ArrToLatin1($unicode)
- {
- if ((!$this->isunicode) || $this->isUnicodeFont())
- {
- return $unicode;
- }
- foreach ($unicode as $char)
- {
- $char = $this->unicode2ascii($char);
- if ($char < 256)
- {
- $outarr[] = $char;
- } else
- {
- $outarr[] = $this->unicode->uni_utf8tolatin[$char];
- } else
- if ($char == 0xFFFD)
- {
- } else
- {
- $outarr[] = 63;
- }
- }
- return $outarr;
- }
- protected function UTF8ToLatin1($str)
- {
- if (!$this->isunicode)
- {
- return $str;
- }
- }
- }
- ?>
I tyle - mam nadzieję, że komuś się przyda :)









Komentarze (12)
b00rt00s / 23 mar 2011 / 17:04
Sorry za offtop, ale na firefoksie4 trochę się twój blog rozjeżdża.
» link «
Czcionki są z grupy nimbus, rozmiar 13. Czcionki nimbus są małe i w standardowym rozmiarze 12pt. na wszystkich innych stronach (np. onet.pl) czcionki są mikroskopijne.
Tak dla porównania wygląda onet przy czcionce 13:
» link «
Korneliusz / 23 mar 2011 / 18:44 Strona www «
Również używam FF4 od pierwszych wersji beta i nie zauważyłem. Myślę, że to kwestia rodziny czcionek o której piszesz. Możesz mi wyłożyć, skąd pomysł wykorzystywania innych krojów / rodzaju czcionek niż użyte na stronie ? Domyślam się, że wyłączyłeś opcję "Pozwalaj stronom stosować innych czcionek ..." wymuszając używanie własnych domyślnych ?
b00rt00s / 23 mar 2011 / 19:23
Po prostu nie miałem zainstalowanej czcionki arial. Dlatego firefox dopasowywał wybraną przeze mnie czcionkę typu sans. Jak doinstalowałem czcionkę arial wszystko wygląda należycie.
Swoją drogą jest to ciekawe, bo w operze pomimo braku czcionki arial wszystko wyświetlane było poprawnie...
Korneliusz / 23 mar 2011 / 19:37 Strona www «
A spróbuj tak ( ja również nie mam Ariala ) : » link «
gordon / 23 mar 2011 / 21:53
U mnie wszystko dziala jak nalezy w ff4.
b00rt00s / 23 mar 2011 / 22:19
No i wiem w czym był problem. Miałem ustawioną minimalną czcionkę na 12 (to są chyba ustawienia domyślne firefoksa). Ustawiłem tak jak Ty i teraz jest ok.
P.S. A nie wiesz z czego może wynikać taka bladość czcionek?
» link «
Gdy czcionką jest arial takich cudów nie ma.
Korneliusz / 23 mar 2011 / 22:46 Strona www «
Trochę dziwne, że miałeś minimalny rozmiar ustawiony na 12. To by znaczyło, że wszystkie o mniejszym rozmiarze były zwiększane do dwunastki O_o
Co do bladości, może hinting, źle dobrany w wygładzaniu? » link « a rozmiary mam takie » link «
b00rt00s / 24 mar 2011 / 00:09
Dobra, nie będę zawracał wam głowy. To nie temat na komentarz w tym wpisie. Będę musiał coś z tymi czcionkami pokombinować. Swoją drogą, to tylko na stronie dobreprogramy.pl jest problem. Po powiększeniu strony czcionki wyglądają ładnie. Są jakby za cienkie. Ale kończmy OT. GSAm poszukam rozwiązania.
Mateusz / 25 mar 2011 / 09:06
Osobiście wolę korzystać z rozwiązań które generują PDF ze źródła w formacie HTML - dompdf, mpdf. Czas generowania dokumentu jest nieco dłuższy, za to drastycznie skraca się czas naszej pracy.
Ostatnio bawiłem się z tym: » link « - projekt renderuje PDF przy pomocy silnika WebKit, co daje ogromne możliwości.
Korneliusz / 25 mar 2011 / 15:16 Strona www «
@Mateusz - tcpdf również generuje z HTMLa. Z wielką chęcią zobaczę dwa pozostałe projekty - przyznam się, że nie miałem okazji do nich jeszcze zaglądać. Ciekawe, czy oprócz konwertowania HTMLa potrafią samodzielną budowę dokumentów (to jest dla mnie bardziej kluczowy wymóg). Jak będę miał chwilę, na pewno zobaczę. Dzięki za ten cenny komentarz.
canis_lupus / 02 kwi 2011 / 13:45
Polecam generowanie PDF z PHP wykorzystując do tego latexa. Bardzo prosto można uzyskać znakomite wizualnie efekty. A PDF wychodzi malutki.
ptb / 29 sie 2011 / 15:02
Generalnie działa OK. W niektórych (rzadko) przypadkach wyrzuca mi taki błąd:
[ Warning ]: array_key_exists() [function.array-key-exists]: The second argument should be either an array or an object
Błąd wyskakuje w funkcji: protected function UTF8ArrToLatin1($unicode) przylinach:
else
if (array_key_exists($char, $this->unicode->uni_utf8tolatin))
{
$outarr[] = $this->unicode->uni_utf8tolatin[$char];
}
Szukałem w źródłowym TCPDF ale nie mogłem znaleźć co to jest za:
$this->unicode->uni_utf8tolatin
Czy macie jakieś sugestie dotyczące rozwiązania tego problemu?