1<?php
2/**
3 * @package dompdf
4 * @link    http://dompdf.github.com/
5 * @author  Benj Carson <benjcarson@digitaljunkies.ca>
6 * @author  Helmut Tischer <htischer@weihenstephan.org>
7 * @author  Fabien Ménager <fabien.menager@gmail.com>
8 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
9 */
10
11namespace Dompdf;
12
13use FontLib\Font;
14
15/**
16 * The font metrics class
17 *
18 * This class provides information about fonts and text.  It can resolve
19 * font names into actual installed font files, as well as determine the
20 * size of text in a particular font and size.
21 *
22 * @static
23 * @package dompdf
24 */
25class FontMetrics
26{
27    /**
28     * Name of the font cache file
29     *
30     * This file must be writable by the webserver process only to update it
31     * with save_font_families() after adding the .afm file references of a new font family
32     * with FontMetrics::saveFontFamilies().
33     * This is typically done only from command line with load_font.php on converting
34     * ttf fonts to ufm with php-font-lib.
35     */
36    const CACHE_FILE = "dompdf_font_family_cache.php";
37
38    /**
39     * @var Canvas
40     * @deprecated
41     */
42    protected $pdf;
43
44    /**
45     * Underlying {@link Canvas} object to perform text size calculations
46     *
47     * @var Canvas
48     */
49    protected $canvas;
50
51    /**
52     * Array of font family names to font files
53     *
54     * Usually cached by the {@link load_font.php} script
55     *
56     * @var array
57     */
58    protected $fontLookup = array();
59
60    /**
61     * @var Options
62     */
63    private $options;
64
65    /**
66     * Class initialization
67     */
68    public function __construct(Canvas $canvas, Options $options)
69    {
70        $this->setCanvas($canvas);
71        $this->setOptions($options);
72        $this->loadFontFamilies();
73    }
74
75    /**
76     * @deprecated
77     */
78    public function save_font_families()
79    {
80        $this->saveFontFamilies();
81    }
82
83    /**
84     * Saves the stored font family cache
85     *
86     * The name and location of the cache file are determined by {@link
87     * FontMetrics::CACHE_FILE}. This file should be writable by the
88     * webserver process.
89     *
90     * @see FontMetrics::loadFontFamilies()
91     */
92    public function saveFontFamilies()
93    {
94        // replace the path to the DOMPDF font directories with the corresponding constants (allows for more portability)
95        $cacheData = sprintf("<?php return array (%s", PHP_EOL);
96        foreach ($this->fontLookup as $family => $variants) {
97            $cacheData .= sprintf("  '%s' => array(%s", addslashes($family), PHP_EOL);
98            foreach ($variants as $variant => $path) {
99                $path = sprintf("'%s'", $path);
100                $path = str_replace('\'' . $this->getOptions()->getFontDir() , '$fontDir . \'' , $path);
101                $path = str_replace('\'' . $this->getOptions()->getRootDir() , '$rootDir . \'' , $path);
102                $cacheData .= sprintf("    '%s' => %s,%s", $variant, $path, PHP_EOL);
103            }
104            $cacheData .= sprintf("  ),%s", PHP_EOL);
105        }
106        $cacheData .= ") ?>";
107        file_put_contents($this->getCacheFile(), $cacheData);
108    }
109
110    /**
111     * @deprecated
112     */
113    public function load_font_families()
114    {
115        $this->loadFontFamilies();
116    }
117
118    /**
119     * Loads the stored font family cache
120     *
121     * @see FontMetrics::saveFontFamilies()
122     */
123    public function loadFontFamilies()
124    {
125        $fontDir = $this->getOptions()->getFontDir();
126        $rootDir = $this->getOptions()->getRootDir();
127
128        // FIXME: temporarily define constants for cache files <= v0.6.2
129        if (!defined("DOMPDF_DIR")) { define("DOMPDF_DIR", $rootDir); }
130        if (!defined("DOMPDF_FONT_DIR")) { define("DOMPDF_FONT_DIR", $fontDir); }
131
132        $file = $rootDir . "/lib/fonts/dompdf_font_family_cache.dist.php";
133        $distFonts = require $file;
134
135        if (!is_readable($this->getCacheFile())) {
136            $this->fontLookup = $distFonts;
137            return;
138        }
139
140        $cacheData = require $this->getCacheFile();
141
142        $this->fontLookup = array();
143        if (is_array($this->fontLookup)) {
144            foreach ($cacheData as $key => $value) {
145                $this->fontLookup[stripslashes($key)] = $value;
146            }
147        }
148
149        // Merge provided fonts
150        $this->fontLookup += $distFonts;
151    }
152
153    /**
154     * @param array $style
155     * @param string $remote_file
156     * @param resource $context
157     * @return bool
158     * @deprecated
159     */
160    public function register_font($style, $remote_file, $context = null)
161    {
162        return $this->registerFont($style, $remote_file);
163    }
164
165    /**
166     * @param array $style
167     * @param string $remoteFile
168     * @param resource $context
169     * @return bool
170     */
171    public function registerFont($style, $remoteFile, $context = null)
172    {
173        $fontname = mb_strtolower($style["family"]);
174        $families = $this->getFontFamilies();
175
176        $entry = array();
177        if (isset($families[$fontname])) {
178            $entry = $families[$fontname];
179        }
180
181        $styleString = $this->getType("{$style['weight']} {$style['style']}");
182        if (isset($entry[$styleString])) {
183            return true;
184        }
185
186        $fontDir = $this->getOptions()->getFontDir();
187        $remoteHash = md5($remoteFile);
188        $localFile = $fontDir . DIRECTORY_SEPARATOR . $remoteHash;
189
190        $cacheEntry = $localFile;
191        $localFile .= ".".strtolower(pathinfo(parse_url($remoteFile, PHP_URL_PATH),PATHINFO_EXTENSION));
192
193        $entry[$styleString] = $cacheEntry;
194
195        // Download the remote file
196        list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);
197        if (false === $remoteFileContent) {
198            return false;
199        }
200
201        $localTempFile = @tempnam($this->options->get("tempDir"), "dompdf-font-");
202        file_put_contents($localTempFile, $remoteFileContent);
203
204        $font = Font::load($localTempFile);
205
206        if (!$font) {
207            unlink($localTempFile);
208            return false;
209        }
210
211        $font->parse();
212        $font->saveAdobeFontMetrics("$cacheEntry.ufm");
213        $font->close();
214
215        unlink($localTempFile);
216
217        if ( !file_exists("$cacheEntry.ufm") ) {
218            return false;
219        }
220
221        // Save the changes
222        file_put_contents($localFile, $remoteFileContent);
223
224        if ( !file_exists($localFile) ) {
225            unlink("$cacheEntry.ufm");
226            return false;
227        }
228
229        $this->setFontFamily($fontname, $entry);
230        $this->saveFontFamilies();
231
232        return true;
233    }
234
235    /**
236     * @param $text
237     * @param $font
238     * @param $size
239     * @param float $word_spacing
240     * @param float $char_spacing
241     * @return float
242     * @deprecated
243     */
244    public function get_text_width($text, $font, $size, $word_spacing = 0.0, $char_spacing = 0.0)
245    {
246        //return self::$_pdf->get_text_width($text, $font, $size, $word_spacing, $char_spacing);
247        return $this->getTextWidth($text, $font, $size, $word_spacing, $char_spacing);
248    }
249
250    /**
251     * Calculates text size, in points
252     *
253     * @param string $text the text to be sized
254     * @param string $font the desired font
255     * @param float $size  the desired font size
256     * @param float $wordSpacing
257     * @param float $charSpacing
258     *
259     * @internal param float $spacing word spacing, if any
260     * @return float
261     */
262    public function getTextWidth($text, $font, $size, $wordSpacing = 0.0, $charSpacing = 0.0)
263    {
264        // @todo Make sure this cache is efficient before enabling it
265        static $cache = array();
266
267        if ($text === "") {
268            return 0;
269        }
270
271        // Don't cache long strings
272        $useCache = !isset($text[50]); // Faster than strlen
273
274        $key = "$font/$size/$wordSpacing/$charSpacing";
275
276        if ($useCache && isset($cache[$key][$text])) {
277            return $cache[$key]["$text"];
278        }
279
280        $width = $this->getCanvas()->get_text_width($text, $font, $size, $wordSpacing, $charSpacing);
281
282        if ($useCache) {
283            $cache[$key][$text] = $width;
284        }
285
286        return $width;
287    }
288
289    /**
290     * @param $font
291     * @param $size
292     * @return float
293     * @deprecated
294     */
295    public function get_font_height($font, $size)
296    {
297        return $this->getFontHeight($font, $size);
298    }
299
300    /**
301     * Calculates font height
302     *
303     * @param string $font
304     * @param float $size
305     *
306     * @return float
307     */
308    public function getFontHeight($font, $size)
309    {
310        return $this->getCanvas()->get_font_height($font, $size);
311    }
312
313    /**
314     * @param $family_raw
315     * @param string $subtype_raw
316     * @return string
317     * @deprecated
318     */
319    public function get_font($family_raw, $subtype_raw = "normal")
320    {
321        return $this->getFont($family_raw, $subtype_raw);
322    }
323
324    /**
325     * Resolves a font family & subtype into an actual font file
326     * Subtype can be one of 'normal', 'bold', 'italic' or 'bold_italic'.  If
327     * the particular font family has no suitable font file, the default font
328     * ({@link Options::defaultFont}) is used.  The font file returned
329     * is the absolute pathname to the font file on the system.
330     *
331     * @param string $familyRaw
332     * @param string $subtypeRaw
333     *
334     * @return string
335     */
336    public function getFont($familyRaw, $subtypeRaw = "normal")
337    {
338        static $cache = array();
339
340        if (isset($cache[$familyRaw][$subtypeRaw])) {
341            return $cache[$familyRaw][$subtypeRaw];
342        }
343
344        /* Allow calling for various fonts in search path. Therefore not immediately
345         * return replacement on non match.
346         * Only when called with NULL try replacement.
347         * When this is also missing there is really trouble.
348         * If only the subtype fails, nevertheless return failure.
349         * Only on checking the fallback font, check various subtypes on same font.
350         */
351
352        $subtype = strtolower($subtypeRaw);
353
354        if ($familyRaw) {
355            $family = str_replace(array("'", '"'), "", strtolower($familyRaw));
356
357            if (isset($this->fontLookup[$family][$subtype])) {
358                return $cache[$familyRaw][$subtypeRaw] = $this->fontLookup[$family][$subtype];
359            }
360
361            return null;
362        }
363
364        $family = "serif";
365
366        if (isset($this->fontLookup[$family][$subtype])) {
367            return $cache[$familyRaw][$subtypeRaw] = $this->fontLookup[$family][$subtype];
368        }
369
370        if (!isset($this->fontLookup[$family])) {
371            return null;
372        }
373
374        $family = $this->fontLookup[$family];
375
376        foreach ($family as $sub => $font) {
377            if (strpos($subtype, $sub) !== false) {
378                return $cache[$familyRaw][$subtypeRaw] = $font;
379            }
380        }
381
382        if ($subtype !== "normal") {
383            foreach ($family as $sub => $font) {
384                if ($sub !== "normal") {
385                    return $cache[$familyRaw][$subtypeRaw] = $font;
386                }
387            }
388        }
389
390        $subtype = "normal";
391
392        if (isset($family[$subtype])) {
393            return $cache[$familyRaw][$subtypeRaw] = $family[$subtype];
394        }
395
396        return null;
397    }
398
399    /**
400     * @param $family
401     * @return null|string
402     * @deprecated
403     */
404    public function get_family($family)
405    {
406        return $this->getFamily($family);
407    }
408
409    /**
410     * @param string $family
411     * @return null|string
412     */
413    public function getFamily($family)
414    {
415        $family = str_replace(array("'", '"'), "", mb_strtolower($family));
416
417        if (isset($this->fontLookup[$family])) {
418            return $this->fontLookup[$family];
419        }
420
421        return null;
422    }
423
424    /**
425     * @param $type
426     * @return string
427     * @deprecated
428     */
429    public function get_type($type)
430    {
431        return $this->getType($type);
432    }
433
434    /**
435     * @param string $type
436     * @return string
437     */
438    public function getType($type)
439    {
440        if (preg_match("/bold/i", $type)) {
441            if (preg_match("/italic|oblique/i", $type)) {
442                $type = "bold_italic";
443            } else {
444                $type = "bold";
445            }
446        } elseif (preg_match("/italic|oblique/i", $type)) {
447            $type = "italic";
448        } else {
449            $type = "normal";
450        }
451
452        return $type;
453    }
454
455    /**
456     * @return array
457     * @deprecated
458     */
459    public function get_font_families()
460    {
461        return $this->getFontFamilies();
462    }
463
464    /**
465     * Returns the current font lookup table
466     *
467     * @return array
468     */
469    public function getFontFamilies()
470    {
471        return $this->fontLookup;
472    }
473
474    /**
475     * @param string $fontname
476     * @param mixed $entry
477     * @deprecated
478     */
479    public function set_font_family($fontname, $entry)
480    {
481        $this->setFontFamily($fontname, $entry);
482    }
483
484    /**
485     * @param string $fontname
486     * @param mixed $entry
487     */
488    public function setFontFamily($fontname, $entry)
489    {
490        $this->fontLookup[mb_strtolower($fontname)] = $entry;
491    }
492
493    /**
494     * @return string
495     */
496    public function getCacheFile()
497    {
498        return $this->getOptions()->getFontDir() . DIRECTORY_SEPARATOR . self::CACHE_FILE;
499    }
500
501    /**
502     * @param Options $options
503     * @return $this
504     */
505    public function setOptions(Options $options)
506    {
507        $this->options = $options;
508        return $this;
509    }
510
511    /**
512     * @return Options
513     */
514    public function getOptions()
515    {
516        return $this->options;
517    }
518
519    /**
520     * @param Canvas $canvas
521     * @return $this
522     */
523    public function setCanvas(Canvas $canvas)
524    {
525        $this->canvas = $canvas;
526        // Still write deprecated pdf for now. It might be used by a parent class.
527        $this->pdf = $canvas;
528        return $this;
529    }
530
531    /**
532     * @return Canvas
533     */
534    public function getCanvas()
535    {
536        return $this->canvas;
537    }
538}