1 <?php
2 namespace SimplePdf;
3
4 /**
5 * Extension of ZendPdf\Page which allows using arbitrary units (e.g. inches or centimeters) and works from
6 * top to bottom, instead of default PDF/PostScript geometry. It also adds some basic formatting functionality,
7 * like word wrap, margins and text alignment.
8 *
9 * @author Robbert Klarenbeek <robbertkl@renbeek.nl>
10 * @copyright 2013 Robbert Klarenbeek
11 * @license http://www.opensource.org/licenses/mit-license.php MIT License
12 */
13 class Page extends \ZendPdf\Page
14 {
15 /**
16 * Constant for left aligned text
17 *
18 * @var float
19 */
20 const TEXT_ALIGN_LEFT = 0;
21
22 /**
23 * Constant for center aligned text
24 *
25 * @var float
26 */
27 const TEXT_ALIGN_CENTER = 0.5;
28
29 /**
30 * Constant for right aligned text
31 *
32 * @var float
33 */
34 const TEXT_ALIGN_RIGHT = 1;
35
36 /**
37 * Constant for point (native) units
38 *
39 * @var string
40 */
41 const UNITS_POINT = 1;
42
43 /**
44 * Constant for inch units (a point is 1/72 of an inch)
45 *
46 * @var string
47 */
48 const UNITS_INCH = 72;
49
50 /**
51 * Constant for centimeter units (72/2.54)
52 *
53 * @var string
54 */
55 const UNITS_CENTIMETER = 28.34645669291339;
56
57 /**
58 * Constant for millimeter units (7.2/2.54)
59 *
60 * @var string
61 */
62 const UNITS_MILLIMETER = 2.834645669291339;
63
64 /**
65 * Factor to convert native units (points) from/to user specified units
66 *
67 * @var float
68 */
69 protected $unitConversion = 1;
70
71 /**
72 * How far lines should be apart vertically, with 1.0 being 'normal' distance
73 *
74 * @var float
75 */
76 protected $lineSpacing = 1.0;
77
78 /**
79 * Margin on the left side of the page, stored in points
80 *
81 * @var float
82 */
83 protected $marginLeft = 0;
84
85 /**
86 * Margin on the right side of the page, stored in points
87 *
88 * @var float
89 */
90 protected $marginRight = 0;
91
92 /**
93 * Margin on the top edge, stored in points
94 *
95 * @var float
96 */
97 protected $marginTop = 0;
98
99 /**
100 * Margin on the bottom edge, stored in points
101 *
102 * @var float
103 */
104 protected $marginBottom = 0;
105
106 /**
107 * Create a new PDF page, with A4 size and default font Helvetica, size 12
108 *
109 * @param string $size page size (see \ZendPdf\Page), default A4 size
110 * @param float $unitConversion conversion factor for custom units, default self::UNITS_CENTIMETER
111 */
112 public function __construct($size = self::SIZE_A4, $unitConversion = self::UNITS_CENTIMETER)
113 {
114 parent::__construct($size);
115 $this->setUnitConversion($unitConversion);
116 $this->setFont(\ZendPdf\Font::fontWithName(\ZendPdf\Font::FONT_HELVETICA), 12);
117 }
118
119 /**
120 * Get the current conversion factor to convert from/to native units (points)
121 *
122 * @return float current conversion factor
123 */
124 public function getUnitConversion()
125 {
126 return $this->unitConversion;
127 }
128
129 /**
130 * Sets the conversion factor to use to convert from/to native units (points)
131 *
132 * @param float $unitConversion new conversion factor
133 */
134 public function setUnitConversion($unitConversion)
135 {
136 $this->unitConversion = $unitConversion;
137 }
138
139 /**
140 * Convert a value in the given units to points
141 *
142 * @param float &$number number (in the given units) to convert, BY REF
143 */
144 public function convertToPoints(&$number)
145 {
146 $number *= $this->getUnitConversion();
147 }
148
149 /**
150 * Convert a value in points to the given units
151 *
152 * @param float &$number number (in points) to convert, BY REF
153 */
154 public function convertFromPoints(&$number)
155 {
156 $number /= $this->getUnitConversion();
157 }
158
159 /**
160 * Convert (x,y)-coordinates in the user space (top to bottom, in the given units, relative to the margins)
161 * to native geometry (points, bottom to top)
162 *
163 * @param float &$x x-coordinate (in the given units, from the left margin) to convert, BY REF
164 * @param float &$y y-coordinate (in the given units, from the top margin) to convert, BY REF
165 */
166 public function convertCoordinatesFromUserSpace(&$x, &$y)
167 {
168 $x += $this->getLeftMargin();
169 $this->convertToPoints($x);
170
171 $y += $this->getTopMargin();
172 $y = $this->getHeight() - $y;
173 $this->convertToPoints($y);
174 }
175
176 /**
177 * Convert (x,y)-coordinates in native geometry (points, bottom to top) to the
178 * user space (top to bottom, in the given units, relative to the margins)
179 *
180 * @param float &$x x-coordinate (in points, from the left) to convert, BY REF
181 * @param float &$y y-coordinate (in points, from the bottom) to convert, BY REF
182 */
183 public function convertCoordinatesToUserSpace(&$x, &$y)
184 {
185 $this->convertFromPoints($x);
186 $x -= $this->getLeftMargin();
187
188 $this->convertFromPoints($y);
189 $y = $this->getHeight() - $y;
190 $x -= $this->getLeftMargin();
191 }
192
193 /**
194 * Get the current line spacing
195 *
196 * @return float line spacing value, 1.0 being 'normal' distance
197 */
198 public function getLineSpacing()
199 {
200 return $this->lineSpacing;
201 }
202
203 /**
204 * Sets the line spacing to use for future writeText() / writeLine() calls
205 *
206 * @param float $lineSpacing new line spacing value to use, 1.0 being 'normal' distance
207 */
208 public function setLineSpacing($lineSpacing)
209 {
210 $this->lineSpacing = $lineSpacing;
211 }
212
213 /**
214 * Get the left margin of the page, in the given units
215 *
216 * @return float left page margin, in the given units
217 */
218 public function getLeftMargin()
219 {
220 $marginLeft = $this->marginLeft;
221 $this->convertFromPoints($marginLeft);
222 return $marginLeft;
223 }
224
225 /**
226 * Set a new left margin, in the given units
227 *
228 * @param float $margin new left margin, in the given units
229 */
230 public function setLeftMargin($margin)
231 {
232 $this->convertToPoints($margin);
233 $this->marginLeft = $margin;
234 }
235
236 /**
237 * Get the right margin of the page, in the given units
238 *
239 * @return float right page margin, in the given units
240 */
241 public function getRightMargin()
242 {
243 $marginRight = $this->marginRight;
244 $this->convertFromPoints($marginRight);
245 return $marginRight;
246 }
247
248 /**
249 * Set a new right margin, in the given units
250 *
251 * @param float $margin new right margin, in the given units
252 */
253 public function setRightMargin($margin)
254 {
255 $this->convertToPoints($margin);
256 $this->marginRight = $margin;
257 }
258
259 /**
260 * Get the top margin of the page, in the given units
261 *
262 * @return float top page margin, in the given units
263 */
264 public function getTopMargin()
265 {
266 $marginTop = $this->marginTop;
267 $this->convertFromPoints($marginTop);
268 return $marginTop;
269 }
270
271 /**
272 * Set a new top margin, in the given units
273 *
274 * @param float $margin new top margin, in the given units
275 */
276 public function setTopMargin($margin)
277 {
278 $this->convertToPoints($margin);
279 $this->marginTop = $margin;
280 }
281
282 /**
283 * Get the bottom margin of the page, in the given units
284 *
285 * @return float bottom page margin, in the given units
286 */
287 public function getBottomMargin()
288 {
289 $marginBottom = $this->marginBottom;
290 $this->convertFromPoints($marginBottom);
291 return $marginBottom;
292 }
293
294 /**
295 * Set a new bottom margin, in the given units
296 *
297 * @param float $margin new bottom margin, in the given units
298 */
299 public function setBottomMargin($margin)
300 {
301 $this->convertToPoints($margin);
302 $this->marginBottom = $margin;
303 }
304
305 /**
306 * Set new margin, in the given units
307 *
308 * @param float $marginLeft new left margin, in the given units
309 * @param float $marginRight new right margin, in the given units
310 * @param float $marginTop new top margin, in the given units
311 * @param float $marginBottom new bottom margin, in the given units
312 */
313 public function setMargins($marginLeft, $marginRight, $marginTop, $marginBottom)
314 {
315 $this->setLeftMargin($marginLeft);
316 $this->setRightMargin($marginRight);
317 $this->setTopMargin($marginTop);
318 $this->setBottomMargin($marginBottom);
319 }
320
321 /**
322 * Set a new margin for all sides, in the given units
323 *
324 * @param float $margin new margin to set on all sides, in the given units
325 */
326 public function setAllMargins($margin)
327 {
328 $this->setMargins($margin, $margin, $margin, $margin);
329 }
330
331 /**
332 * Get page width in the given units
333 *
334 * @return float width of the page in the given units
335 */
336 public function getWidth()
337 {
338 $width = parent::getWidth();
339 $this->convertFromPoints($width);
340 return $width;
341 }
342
343 /**
344 * Get the width (in the given units) of the page area excluding the set margins (if any)
345 *
346 * @return float page width in the given units, excluding margins
347 */
348 public function getInnerWidth()
349 {
350 return $this->getWidth() - $this->getLeftMargin() - $this->getRightMargin();
351 }
352
353 /**
354 * Get page height in the given units
355 *
356 * @return float height of the page in the given units
357 */
358 public function getHeight()
359 {
360 $height = parent::getHeight();
361 $this->convertFromPoints($height);
362 return $height;
363 }
364
365 /**
366 * Get the height (in the given units) of the page area excluding the set margins (if any)
367 *
368 * @return float page height in the given units, excluding margins
369 */
370 public function getInnerHeight()
371 {
372 return $this->getHeight() - $this->getTopMargin() - $this->getBottomMargin();
373 }
374
375 /**
376 * Sets a new font family and, optionally, a new font size as well
377 *
378 * @param \ZendPdf\Resource\Font\AbstractFont $font font object to use
379 * @param float $fontSize new font size, leave it out to keep the current font size
380 */
381 public function setFont(\ZendPdf\Resource\Font\AbstractFont $font, $fontSize = null)
382 {
383 if (is_null($fontSize)) {
384 $fontSize = $this->getFontSize();
385 }
386
387 parent::setFont($font, $fontSize);
388 }
389
390 /**
391 * Change the font size, without changing the font family
392 *
393 * @param float $fontSize new font size to use
394 */
395 public function setFontSize($fontSize)
396 {
397 $this->setFont($this->getFont(), $fontSize);
398 }
399
400 /**
401 * Draw a line from 1 point to another
402 *
403 * @param float $x1 x-coordinate (in the given units) of the point from where to draw the line
404 * @param float $y1 y-coordinate (in the given units) of the point from where to draw the line
405 * @param float $x2 x-coordinate (in the given units) of the point to where to draw the line
406 * @param float $y2 y-coordinate (in the given units) of the point to where to draw the line
407 */
408 public function drawLine($x1, $y1, $x2, $y2)
409 {
410 $this->convertCoordinatesFromUserSpace($x1, $y1);
411 $this->convertCoordinatesFromUserSpace($x2, $y2);
412 parent::drawLine($x1, $y1, $x2, $y2);
413 }
414
415 /**
416 * Write a (multiline / optionally wrapping) text to the page
417 *
418 * @param float $x x-coordinate (in the given units) of the anchor point of the text
419 * @param float $y y-coordinate (in the given units) of the anchor point of the text
420 * @param string $text text to write to the PDF (can contain newlines)
421 * @param float $anchorPoint horizontal position (0..1) to anchor each line, defaults to self::TEXT_ALIGN_LEFT
422 * @param float $wrapWidth width (in the given units) to wrap text at, or leave out for no wrapping
423 */
424 public function writeText($x, $y, $text, $anchorPoint = self::TEXT_ALIGN_LEFT, $wrapWidth = 0)
425 {
426 if ($wrapWidth > 0) {
427 $text = $this->wordWrapText($text, $wrapWidth);
428 }
429
430 $lineHeight = $this->getLineHeight();
431 foreach (explode(PHP_EOL, $text) as $index => $line) {
432 if (empty($line)) {
433 continue;
434 }
435
436 $anchorOffset = ($anchorPoint == 0) ? 0 : -$anchorPoint * $this->getTextWidth($line);
437 $this->writeLine($x + $anchorOffset, $y + $index * $lineHeight, $line);
438 }
439 }
440
441 /**
442 * Write a single line of text to the page
443 *
444 * @param float $x x-coordinate (in the given units) of the top-left corner where the text should start
445 * @param float $y y-coordinate (in the given units) of the top-left corner where the text should start
446 * @param string $line line to write to the page, should not contain newlines (and will NOT be wrapped)
447 */
448 public function writeLine($x, $y, $line)
449 {
450 $this->convertCoordinatesFromUserSpace($x, $y);
451 $y -= $this->getFontSize();
452 $this->drawText($line, $x, $y, 'UTF-8');
453 }
454
455 /**
456 * Word-wrap a text to a certain width, using the current font properties
457 *
458 * @param string $text text to wrap (can already contain some newlines)
459 * @param string $wrapWidth width (in the given units) to wrap the text to
460 * @return string the same text but with newlines inserted at the specified $wrapWidth
461 */
462 public function wordWrapText($text, $wrapWidth)
463 {
464 $wrappedText = '';
465 foreach (explode(PHP_EOL, $text) as $line) {
466 $words = explode(' ', $line);
467 $currentLine = array_shift($words);
468 while (count($words) > 0) {
469 $word = array_shift($words);
470 if ($this->getTextWidth($currentLine . ' ' . $word) > $wrapWidth) {
471 $wrappedText .= PHP_EOL . $currentLine;
472 $currentLine = $word;
473 } else {
474 $currentLine .= ' ' . $word;
475 }
476 }
477 $wrappedText .= PHP_EOL . $currentLine;
478 }
479 return ltrim($wrappedText, PHP_EOL);
480 }
481
482 /**
483 * Get the line height (the offset between consecutive lines)
484 *
485 * @return float distance between consecutive lines in the given units
486 */
487 public function getLineHeight()
488 {
489 $lineHeight = $this->getFontSize() * 1.2 * $this->getLineSpacing();
490 $this->convertFromPoints($lineHeight);
491 return $lineHeight;
492 }
493
494 /**
495 * Calculates how much (horizontal) space a text would use if written to the page, using
496 * the current font properties
497 *
498 * @param string $text text to calculate the width for (should not contain newlines)
499 * @return float width (in the given units) that the text would use if written to the page
500 */
501 public function getTextWidth($text)
502 {
503 $font = $this->getFont();
504 $fontSize = $this->getFontSize();
505 $text = iconv('UTF-8', 'UTF-16BE', $text);
506 $chars = array();
507 for ($i = 0; $i < strlen($text); $i++) {
508 $chars[] = (ord($text[$i++]) << 8) | ord($text[$i]);
509 }
510 $glyphs = $font->glyphNumbersForCharacters($chars);
511 $widths = $font->widthsForGlyphs($glyphs);
512 $textWidth = $fontSize * array_sum($widths) / $font->getUnitsPerEm();
513 $this->convertFromPoints($textWidth);
514 return $textWidth;
515 }
516 }
517