1: <?php
2: /**
3: * Stack.php
4: *
5: * @since 2011-05-23
6: * @category Library
7: * @package PdfFont
8: * @author Nicola Asuni <info@tecnick.com>
9: * @copyright 2011-2015 Nicola Asuni - Tecnick.com LTD
10: * @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
11: * @link https://github.com/tecnickcom/tc-lib-pdf-font
12: *
13: * This file is part of tc-lib-pdf-font software library.
14: */
15:
16: namespace Com\Tecnick\Pdf\Font;
17:
18: use \Com\Tecnick\Pdf\Font\Font;
19: use \Com\Tecnick\Pdf\Font\Exception as FontException;
20:
21: /**
22: * Com\Tecnick\Pdf\Font\Stack
23: *
24: * @since 2011-05-23
25: * @category Library
26: * @package PdfFont
27: * @author Nicola Asuni <info@tecnick.com>
28: * @copyright 2011-2015 Nicola Asuni - Tecnick.com LTD
29: * @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
30: * @link https://github.com/tecnickcom/tc-lib-pdf-font
31: */
32: class Stack extends \Com\Tecnick\Pdf\Font\Buffer
33: {
34: /**
35: * Default font size in points
36: */
37: const DEFAULT_SIZE = 10;
38:
39: /**
40: * Array (stack) containing fonts in order of insertion.
41: * The last item is the current font.
42: *
43: * @var array
44: */
45: protected $stack = array();
46:
47: /**
48: * Current font index
49: *
50: * @var int
51: */
52: protected $index = -1;
53:
54: /**
55: * Array containing font metrics for each fontkey-size combination.
56: *
57: * @var array
58: */
59: protected $metric = array();
60:
61: /**
62: * Insert a font into the stack
63: *
64: * The definition file (and the font file itself when embedding) must be present either in the current directory
65: * or in the one indicated by K_PATH_FONTS if the constant is defined.
66: *
67: * @param int $objnum Current PDF object number
68: * @param string $font Font family, or comma separated list of font families
69: * If it is a standard family name, it will override the corresponding font.
70: * @param string $style Font style.
71: * Possible values are (case insensitive):
72: * regular (default)
73: * B: bold
74: * I: italic
75: * U: underline
76: * D: strikeout (linethrough)
77: * O: overline
78: * @param int $size Font size in points (set to null to inherit the last font size).
79: * @param float $spacing Extra spacing between characters.
80: * @param float $stretching Horizontal character stretching ratio.
81: * @param string $ifile The font definition file (or empty for autodetect).
82: * By default, the name is built from the family and style, in lower case with no spaces.
83: * @param bool $subset If true embedd only a subset of the font
84: * (stores only the information related to the used characters);
85: * If false embedd full font;
86: * This option is valid only for TrueTypeUnicode fonts and it is disabled for PDF/A.
87: * If you want to enable users to modify the document, set this parameter to false.
88: * If you subset the font, the person who receives your PDF would need to have
89: * your same font in order to make changes to your PDF.
90: * The file size of the PDF would also be smaller because you are embedding only a subset.
91: * Set this to null to use the default value.
92: * NOTE: This option is computational and memory intensive.
93: *
94: * @return array Font data
95: *
96: * @throws FontException in case of error
97: */
98: public function insert(
99: &$objnum,
100: $font,
101: $style = '',
102: $size = null,
103: $spacing = null,
104: $stretching = null,
105: $ifile = '',
106: $subset = null
107: ) {
108: if ($subset === null) {
109: $subset = $this->subset;
110: }
111: $size = $this->getInputSize($size);
112: $spacing = $this->getInputSpacing($spacing);
113: $stretching = $this->getInputStretching($stretching);
114:
115: // try to load the corresponding imported font
116: $err = null;
117: $keys = $this->getNormalizedFontKeys($font);
118: $fontkey = '';
119: foreach ($keys as $fkey) {
120: try {
121: $fontkey = $this->add($objnum, $fkey, $style, $ifile, $subset);
122: $err = null;
123: break;
124: } catch (FontException $exc) {
125: $err = $exc;
126: }
127: }
128: if ($err !== null) {
129: throw new FontException($err->getMessage());
130: }
131:
132: // add this font in the stack
133: $data = $this->getFont($fontkey);
134:
135: $this->stack[++$this->index] = array(
136: 'key' => $fontkey,
137: 'style' => $data['style'],
138: 'size' => $size,
139: 'spacing' => $spacing,
140: 'stretching' => $stretching,
141: );
142:
143: return $this->getFontMetric($this->stack[$this->index]);
144: }
145:
146: /**
147: * Returns the current font data array
148: *
149: * @return array
150: */
151: public function getCurrentFont()
152: {
153: return $this->getFontMetric($this->stack[$this->index]);
154: }
155:
156: /**
157: * Remove and return the last inserted font
158: *
159: * @return array
160: */
161: public function popLastFont()
162: {
163: if ($this->index < 0) {
164: throw new FontException('The font stack is empty');
165: }
166: $font = array_pop($this->stack);
167: --$this->index;
168: return $this->getFontMetric($font);
169: }
170:
171: /**
172: * Replace missing characters with selected substitutions
173: *
174: * @param array $uniarr Array of character codepoints.
175: * @param array $subs Array of possible character substitutions.
176: * The key is the character to check (integer value),
177: * the value is an array of possible substitutes.
178: *
179: * @return array
180: */
181: public function replaceMissingChars(array $uniarr, array $subs = array())
182: {
183: $font = $this->getFontMetric($this->stack[$this->index]);
184: foreach ($uniarr as $pos => $uni) {
185: if (isset($font['cw'][$uni]) || !isset($subs[$uni])) {
186: continue;
187: }
188: foreach ($subs[$uni] as $alt) {
189: if (isset($font['cw'][$alt])) {
190: $uniarr[$pos] = $alt;
191: break;
192: }
193: }
194: }
195: return $uniarr;
196: }
197:
198: /**
199: * Returns true if the specified unicode value is defined in the current font
200: *
201: * @param int $ord Unicode character value to convert
202: *
203: * @return bool
204: */
205: public function isCharDefined($ord)
206: {
207: $font = $this->getFontMetric($this->stack[$this->index]);
208: return isset($font['cw'][$ord]);
209: }
210:
211: /**
212: * Returns true if the specified unicode value is defined in the current font
213: *
214: * @param int $ord Unicode character value.
215: *
216: * @return int
217: */
218: public function getCharWidth($ord)
219: {
220: if ($ord == 173) {
221: // SHY character is not printed, as it is used for text hyphenation
222: return 0;
223: }
224: $font = $this->getFontMetric($this->stack[$this->index]);
225: if (isset($font['cw'][$ord])) {
226: return $font['cw'][$ord];
227: }
228: return $font['dw'];
229: }
230:
231: /**
232: * Returns the lenght of the string specified using an array of codepoints.
233: *
234: * @param array $uniarr Array of character codepoints.
235: *
236: * @return float
237: */
238: public function getOrdArrWidth($uniarr)
239: {
240: $width = 0;
241: foreach ($uniarr as $ord) {
242: $width += $this->GetCharWidth($ord);
243: }
244: $width += ($this->stack[$this->index]['spacing']
245: * $this->stack[$this->index]['stretching']
246: * (count($uniarr) - 1)
247: );
248: return $width;
249: }
250:
251: /**
252: * Returns the glyph bounding box of the specified character in the current font in user units.
253: *
254: * @param int $ord Unicode character value.
255: *
256: * @return array (xMin, yMin, xMax, yMax)
257: */
258: public function getCharBBox($ord)
259: {
260: $font = $this->getFontMetric($this->stack[$this->index]);
261: if (isset($font['cbbox'][$ord])) {
262: return $font['cbbox'][$ord];
263: }
264: return array(0, 0, 0, 0); // glyph without outline
265: }
266:
267: /**
268: * Replace a char if it is defined on the current font.
269: *
270: * @param int $oldchar Integer code (unicode) of the character to replace.
271: * @param int $newchar Integer code (unicode) of the new character.
272: *
273: * @return int the replaced char or the old char in case the new char i not defined
274: */
275: public function replaceChar($oldchar, $newchar)
276: {
277: if ($this->isCharDefined($newchar)) {
278: // add the new char on the subset list
279: $this->addSubsetChar($this->stack[$this->index]['key'], $newchar);
280: // return the new character
281: return $newchar;
282: }
283: // return the old char
284: return $oldchar;
285: }
286:
287: /**
288: * Returns the font metrics associated to the input key.
289: *
290: * @param array $font Stack item
291: *
292: * @return array
293: */
294: protected function getFontMetric($font)
295: {
296: $mkey = md5(serialize($font));
297: if (!empty($this->metric[$mkey])) {
298: return $this->metric[$mkey];
299: }
300:
301: $usize = ((float) $font['size'] / $this->kunit);
302: $cratio = ($usize / 1000);
303: $wratio = ($cratio * $font['stretching']); // horizontal ratio
304: $data = $this->getFont($font['key']);
305:
306: // add this font in the stack wit metrics in internal units
307: $this->metric[$mkey] = array(
308: 'out' => sprintf('BT /F%d %F Tf ET', $data['i'], $font['size']), // PDF output string
309: 'key' => $font['key'],
310: 'size' => $font['size'], // size in points
311: 'spacing' => $font['spacing'],
312: 'stretching' => $font['stretching'],
313: 'usize' => $usize, // size in internal units
314: 'cratio' => $cratio, // conversion ratio
315: 'up' => ($data['up'] * $cratio),
316: 'ut' => ($data['ut'] * $cratio),
317: 'dw' => ($data['dw'] * $cratio * $font['stretching']),
318: 'ascent' => ($data['desc']['Ascent'] * $cratio),
319: 'descent' => ($data['desc']['Descent'] * $cratio),
320: 'capheight' => ($data['desc']['CapHeight'] * $cratio),
321: 'xheight' => ($data['desc']['XHeight'] * $cratio),
322: 'avgwidth' => ($data['desc']['AvgWidth'] * $cratio * $font['stretching']),
323: 'maxwidth' => ($data['desc']['MaxWidth'] * $cratio * $font['stretching']),
324: 'missingwidth' => ($data['desc']['MissingWidth'] * $cratio * $font['stretching']),
325: 'cw' => array(),
326: 'cbbox' => array(),
327: );
328: $tbox = explode(' ', substr($data['desc']['FontBBox'], 1, -1));
329: $this->metric[$mkey]['fbbox'] = array(
330: ($tbox[0] * $wratio), // left
331: ($tbox[1] * $cratio), // bottom
332: ($tbox[2] * $wratio), // right
333: ($tbox[3] * $cratio), // top
334: );
335: //left, bottom, right, and top edges
336: foreach ($data['cw'] as $chr => $width) {
337: $this->metric[$mkey]['cw'][$chr] = ($width * $wratio);
338: }
339: foreach ($data['cbbox'] as $chr => $val) {
340: $this->metric[$mkey]['cbbox'][$chr] = array(
341: ($val[0] * $wratio), // left
342: ($val[1] * $cratio), // bottom
343: ($val[2] * $wratio), // right
344: ($val[3] * $cratio), // top
345: );
346: }
347:
348: return $this->metric[$mkey];
349: }
350:
351: /**
352: * Normalize the input size
353: *
354: * return float
355: */
356: protected function getInputSize($size = null)
357: {
358: if ($size === null) {
359: if ($this->index >= 0) {
360: // inherit the size of the last inserted font
361: return $this->stack[$this->index]['size'];
362: } else {
363: return self::DEFAULT_SIZE;
364: }
365: }
366: return max(0, (float) $size);
367: }
368:
369: /**
370: * Normalize the input spacing
371: *
372: * @param float $spacing Extra spacing between characters.
373: *
374: * return float
375: */
376: protected function getInputSpacing($spacing = null)
377: {
378: if ($spacing === null) {
379: if ($this->index >= 0) {
380: // inherit the size of the last inserted font
381: return $this->stack[$this->index]['spacing'];
382: } else {
383: return 0;
384: }
385: }
386: return ((float) $spacing);
387: }
388:
389: /**
390: * Normalize the input stretching
391: *
392: * @param float $stretching Horizontal character stretching ratio.
393: *
394: * return float
395: */
396: protected function getInputStretching($stretching = null)
397: {
398: if ($stretching === null) {
399: if ($this->index >= 0) {
400: // inherit the size of the last inserted font
401: return $this->stack[$this->index]['stretching'];
402: } else {
403: return 1;
404: }
405: }
406: return ((float) $stretching);
407: }
408:
409: /**
410: * Return normalized font keys
411: *
412: * @param string $fontfamily Property string containing comma-separated font family names
413: *
414: * @return array
415: */
416: protected function getNormalizedFontKeys($fontfamily)
417: {
418: $keys = array();
419: // remove spaces and symbols
420: $fontfamily = preg_replace('/[^a-z0-9_\,]/', '', strtolower($fontfamily));
421: // extract all font names
422: $fontslist = preg_split('/[,]/', $fontfamily);
423: // replacement patterns
424: $pattern = array('/^serif|^cursive|^fantasy|^timesnewroman/', '/^sansserif/', '/^monospace/');
425: $replacement = array('times', 'helvetica', 'courier');
426: // find first valid font name
427: foreach ($fontslist as $font) {
428: // replace font variations
429: $font = preg_replace('/regular$/', '', $font);
430: $font = preg_replace('/italic$/', 'I', $font);
431: $font = preg_replace('/oblique$/', 'I', $font);
432: $font = preg_replace('/bold([I]?)$/', 'B\\1', $font);
433: // replace common family names and core fonts
434: $keys[] = preg_replace($pattern, $replacement, $font);
435: }
436: return $keys;
437: }
438: }
439: