1<?php
2// $Header: /cvsroot/html2ps/output.png.class.php,v 1.7 2007/05/07 13:12:07 Konstantin Exp $
3
4require_once(HTML2PS_DIR.'ot.class.php');
5require_once(HTML2PS_DIR.'path.php');
6require_once(HTML2PS_DIR.'font_factory.class.php');
7
8/**
9 * TODO: of course, it is not 'real' affine transformation;
10 * it is just a compatibility hack
11 */
12class AffineTransform {
13  var $_y_offset;
14  var $_x_scale;
15  var $_y_scale;
16
17  function AffineTransform($y_offset, $x_scale, $y_scale) {
18    $this->_y_offset = $y_offset;
19    $this->_x_scale = $x_scale;
20    $this->_y_scale = $y_scale;
21  }
22
23  function apply(&$x, &$y) {
24    $x = floor($x * $this->_x_scale);
25    $y = floor($this->_y_offset - $y * $this->_y_scale);
26  }
27}
28
29class OutputDriverPNG extends OutputDriverGeneric {
30  var $_image;
31
32  var $_clipping;
33
34  var $_media;
35  var $_heightPixels;
36  var $_widthPixels;
37  var $_color;
38  var $_font;
39  var $_path;
40
41  /**
42   * This variable  contains an  array of clipping  contexts. Clipping
43   * context describes the "active area" and "base" image (image which
44   * will take the changes drawn in clipped area).
45   *
46   * As GD does not support  clipping natively, when new clipping area
47   * is  defined,  we  create  new  image. When  clipping  context  is
48   * terminated (i.e. by establishing new clipping context, by call to
49   * 'restore' or by finishing the image output), only area bounded by
50   * clipping region  is copied  to the "base"  image. Note  that This
51   * will  increase the  memory  consumption, as  we'll  need to  keep
52   * several active images at once.
53   */
54  var $_clip;
55
56  function _restoreColor() {
57    imagecolordeallocate($this->_image, $this->_color[0]);
58    array_shift($this->_color);
59  }
60
61  function _restoreClip() {
62    /**
63     * As clipping context images have the same size/scale, we may use
64     * the simplest/fastest image copying function
65     */
66    $clip = $this->_clipping[0];
67    imagecopy($clip['image'],
68              $this->_image,
69              $clip['box']->ll->x,
70              $clip['box']->ll->y,
71              $clip['box']->ll->x,
72              $clip['box']->ll->y,
73              $clip['box']->getWidth(),
74              $clip['box']->getHeight());
75
76    /**
77     * Now we should free image allocated for the clipping context to avoid memory leaks
78     */
79    imagedestroy($this->_image);
80    $this->_image = $clip['image'];
81
82    /**
83     * Remove clipping context from the stack
84     */
85    array_shift($this->_clipping);
86  }
87
88  function _saveColor($rgb) {
89    $color = imagecolorallocate($this->_image, $rgb[0], $rgb[1], $rgb[2]);
90    array_unshift($this->_color, array('rgb'    => $rgb,
91                                       'object' => $color));
92  }
93
94  function _saveClip($box) {
95    /**
96     * Initialize clipping  context record and add it  to the clipping
97     * stack
98     */
99    $clip = array('image' => $this->_image,
100                  'box'   => $box);
101    array_unshift($this->_clipping, $clip);
102
103    /**
104     * Create a copy of current image for the clipping context
105     */
106    $width  = imagesx($clip['image']);
107    $height = imagesy($clip['image']);
108    $this->_image = imagecreatetruecolor($width,
109                                         $height);
110    imagecopy($this->_image,
111              $clip['image'],
112              0,0,
113              0,0,
114              $width, $height);
115  }
116
117  function _getCurrentColor() {
118    return $this->_color[0]['object'];
119  }
120
121  function _setColor($color) {
122    imagecolordeallocate($this->_image, $this->_color[0]['object']);
123    $this->_color[0] = $color;
124  }
125
126  function _setFont($typeface, $encoding, $size) {
127    global $g_font_resolver_pdf;
128    $fontfile = $g_font_resolver_pdf->ttf_mappings[$typeface];
129
130    $font = $this->_font_factory->getTrueType($typeface, $encoding);
131    $ascender = $font->ascender() / 1000;
132
133    $this->_font[0] = array('font'     => $typeface,
134                            'encoding' => $encoding,
135                            'size'     => $size,
136                            'ascender' => $ascender);
137  }
138
139  function _getFont() {
140    return $this->_font[0];
141  }
142
143  function _drawLine($x1, $y1, $x2, $y2) {
144    imageline($this->_image, $x1, $y1, $x2, $y2, $this->_color[0]['object']);
145  }
146
147  /**
148   * Note that "paper space" have Y coordinate axis directed to the bottom,
149   * while images have Y coordinate axis directory to the top
150   */
151  function _fixCoords(&$x, &$y) {
152    $x = $this->_fixCoordX($x);
153    $y = $this->_fixCoordY($y);
154  }
155
156  function _fixCoordX($source_x) {
157    $x = $source_x;
158    $dummy = 0;
159    $this->_transform->apply($x, $dummy);
160    return $x;
161  }
162
163  function _fixCoordY($source_y) {
164    $y = $source_y;
165    $dummy = 0;
166    $this->_transform->apply($dummy, $y);
167    return $y;
168  }
169
170  function _fixSizes(&$x, &$y) {
171    $x = $this->_fixSizeX($x);
172    $y = $this->_fixSizeY($y);
173  }
174
175  function _fixSizeX($x) {
176    static $scale = null;
177    if (is_null($scale)) { $scale = $this->_widthPixels / mm2pt($this->media->width()); };
178    return ceil($x * $scale);
179  }
180
181  function _fixSizeY($y) {
182    static $scale = null;
183    if (is_null($scale)) { $scale = $this->_heightPixels / mm2pt($this->media->height()); };
184    return ceil($y * $scale);
185  }
186
187  function OutputDriverPNG() {
188    $this->OutputDriverGeneric();
189
190    $this->_color    = array();
191    $this->_font     = array();
192    $this->_path     = new Path;
193    $this->_clipping = array();
194
195    $this->_font_factory = new FontFactory();
196  }
197
198  function reset(&$media) {
199    parent::reset($media);
200
201    $this->update_media($media);
202  }
203
204  function update_media($media) {
205    parent::update_media($media);
206
207    /**
208     * Here we use a small hack; media height and width (in millimetres) match
209     * the size of screenshot (in pixels), so we take them as-is
210     */
211    $this->_heightPixels = $media->height();
212    $this->_widthPixels  = $media->width();
213
214    $this->_image = imagecreatetruecolor($this->_widthPixels,
215                                         $this->_heightPixels);
216    /**
217     * Render white background
218     */
219    $white = imagecolorallocate($this->_image, 255,255,255);
220    imagefill($this->_image, 0,0,$white);
221    imagecolordeallocate($this->_image, $white);
222
223    $this->_color[0] = array('rgb'    => array(0,0,0),
224                             'object' => imagecolorallocate($this->_image, 0,0,0));
225
226    /**
227     * Setup initial clipping region
228     */
229    $this->_clipping = array();
230    $this->_saveClip(new Rectangle(new Point(0,
231                                             0),
232                                   new Point($this->_widthPixels-1,
233                                             $this->_heightPixels-1)));
234
235    $this->_transform = new AffineTransform($this->_heightPixels,
236                                            $this->_widthPixels / mm2pt($this->media->width()),
237                                            $this->_heightPixels / mm2pt($this->media->height()));
238  }
239
240  function add_link($x, $y, $w, $h, $target) { /* N/A */ }
241  function add_local_link($left, $top, $width, $height, $anchor) { /* N/A */ }
242
243  function circle($x, $y, $r) {
244    $this->_path = new PathCircle();
245    $this->_path->set_r($r);
246    $this->_path->set_x($x);
247    $this->_path->set_y($y);
248  }
249
250  function clip() {
251    /**
252     * Only  rectangular  clipping  areas  are  supported;  we'll  use
253     * bounding box of  current path for clipping. If  current path is
254     * an rectangle, bounding box will match the path itself.
255     */
256    $box = $this->_path->getBbox();
257
258    /**
259     * Convert bounding from media coordinates
260     * to output device coordinates
261     */
262    $this->_fixCoords($box->ll->x, $box->ll->y);
263    $this->_fixCoords($box->ur->x, $box->ur->y);
264    $box->normalize();
265
266    /**
267     * Add a clipping context information
268     */
269    $this->_restoreClip();
270    $this->_saveClip($box);
271
272    /**
273     * Reset path after clipping have been applied
274     */
275    $this->_path = new Path;
276  }
277
278  function close() {
279    /**
280     * A small hack; as clipping  context is save every time 'save' is
281     * called, we may deterine the number of graphic contexts saved by
282     * the size of clipping context stack
283     */
284    while (count($this->_clipping) > 0) {
285      $this->restore();
286    };
287
288    imagepng($this->_image, $this->get_filename());
289    imagedestroy($this->_image);
290  }
291
292  function closepath() {
293    $this->_path->close();
294  }
295
296  function content_type() {
297    return ContentType::png();
298  }
299
300  function dash($x, $y) { }
301  function decoration($underline, $overline, $strikeout) { }
302
303  function error_message() {
304    return "OutputDriverPNG: generic error";
305  }
306
307  function field_multiline_text($x, $y, $w, $h, $value, $field_name) { /* N/A */ }
308  function field_text($x, $y, $w, $h, $value, $field_name) { /* N/A */ }
309  function field_password($x, $y, $w, $h, $value, $field_name) { /* N/A */ }
310  function field_pushbutton($x, $y, $w, $h) { /* N/A */ }
311  function field_pushbuttonimage($x, $y, $w, $h, $field_name, $value, $actionURL) { /* N/A */ }
312  function field_pushbuttonreset($x, $y, $w, $h) { /* N/A */ }
313  function field_pushbuttonsubmit($x, $y, $w, $h, $field_name, $value, $actionURL) { /* N/A */ }
314  function field_checkbox($x, $y, $w, $h, $name, $value) { /* N/A */ }
315  function field_radio($x, $y, $w, $h, $groupname, $value, $checked) { /* N/A */ }
316  function field_select($x, $y, $w, $h, $name, $value, $options) { /* N/A */ }
317
318  function fill() {
319    $this->_path->fill($this->_transform, $this->_image, $this->_getCurrentColor());
320    $this->_path = new Path;
321  }
322
323  function font_ascender($name, $encoding) {
324    $font = $this->_font_factory->getTrueType($name, $encoding);
325    return $font->ascender() / 1000;
326  }
327
328  function font_descender($name, $encoding) {
329    $font = $this->_font_factory->getTrueType($name, $encoding);
330    return -$font->descender() / 1000;
331  }
332
333  function get_bottom() {}
334
335  /**
336   * Image output always contains only one page
337   */
338  function get_expected_pages() {
339    return 1;
340  }
341
342  function image($image, $x, $y, $scale) {
343    $this->image_scaled($image, $x, $y, $scale, $scale);
344  }
345
346  function image_scaled($image, $x, $y, $scale_x, $scale_y) {
347    $this->_fixCoords($x, $y);
348
349    $sx = $image->sx();
350    $sy = $image->sy();
351
352    /**
353     * Get image size in device coordinates
354     */
355    $dx = $sx*$scale_x;
356    $dy = $sy*$scale_y;
357    $this->_fixSizes($dx, $dy);
358
359    imagecopyresampled($this->_image, $image->get_handle(),
360                       $x, $y-$dy,
361                       0, 0,
362                       $dx, $dy,
363                       $sx, $sy);
364  }
365
366  function image_ry($image, $x, $y, $height, $bottom, $ox, $oy, $scale) {
367    $base_y = floor($this->_fixCoordY($bottom));
368    $this->_fixCoords($x, $y);
369    $dest_height = floor($this->_fixSizeY($height));
370    $start_y = $y - $dest_height;
371
372    $sx = $image->sx();
373    $sy = $image->sy();
374    $dx = $this->_fixSizeX($sx * $scale);
375    $dy = $this->_fixSizeY($sy * $scale);
376
377    $cx = $x;
378    $cy = $start_y - ceil($this->_fixSizeY($oy) / $dest_height) * $dest_height;
379    while ($cy < $base_y) {
380      imagecopyresampled($this->_image, $image->get_handle(),
381                         $cx, $cy,
382                         0, 0,
383                         $dx, $dy,
384                         $sx, $sy);
385      $cy += $dest_height;
386    };
387  }
388
389  function image_rx($image, $x, $y, $width, $right, $ox, $oy, $scale) {
390    $base_x = floor($this->_fixCoordX($right));
391    $this->_fixCoords($x, $y);
392    $dest_width = floor($this->_fixSizeX($width));
393    $start_x = $x - $dest_width;
394
395    $sx = $image->sx();
396    $sy = $image->sy();
397    $dx = $this->_fixSizeX($sx * $scale);
398    $dy = $this->_fixSizeY($sy * $scale);
399
400    $cx = $start_x - ceil($this->_fixSizeX($oy) / $dest_width) * $dest_width;
401
402    $cy = $y - $dy;
403
404    while ($cx < $base_x) {
405      imagecopyresampled($this->_image, $image->get_handle(),
406                         $cx, $cy,
407                         0, 0,
408                         $dx, $dy,
409                         $sx, $sy);
410      $cx += $dest_width;
411    };
412  }
413
414  function image_rx_ry($image, $x, $y, $width, $height, $right, $bottom, $ox, $oy, $scale) {
415    $base_x = floor($this->_fixCoordX($right));
416    $base_y = floor($this->_fixCoordY($bottom));
417    $this->_fixCoords($x, $y);
418    $dest_width  = floor($this->_fixSizeX($width));
419    $dest_height = floor($this->_fixSizeY($height));
420    $start_x = $x - $dest_width;
421    $start_y = $y - $dest_height;
422
423    $sx = $image->sx();
424    $sy = $image->sy();
425    $dx = $this->_fixSizeX($sx * $scale);
426    $dy = $this->_fixSizeY($sy * $scale);
427
428    $cx = $start_x - ceil($this->_fixSizeX($ox) / $dest_width)  * $dest_width;
429    $cy = $start_y - ceil($this->_fixSizeY($oy) / $dest_height) * $dest_height;
430
431    while ($cy < $base_y) {
432      while ($cx < $base_x) {
433        imagecopyresampled($this->_image,
434                           $image->get_handle(),
435                           $cx, $cy,
436                           0, 0,
437                           $dx, $dy,
438                           $sx, $sy);
439        $cx += $dest_width;
440      };
441      $cx = $start_x - ceil($this->_fixSizeX($ox) / $dest_width)  * $dest_width;
442      $cy += $dest_height;
443    };
444  }
445
446  function lineto($x, $y) {
447    $this->_path->addPoint(new Point($x, $y));
448  }
449
450  function moveto($x, $y) {
451    $this->_path->clear();
452    $this->_path->addPoint(new Point($x, $y));
453  }
454
455  function new_form($name) { /* N/A */ }
456  function next_page() { /* N/A */ }
457  function release() { }
458
459  /**
460   * Note: _restoreClip  will change current image object,  so we must
461   * release all  image-dependent objects before  call to _restoreClip
462   * to ensure resources are released correctly
463   */
464  function restore() {
465    $this->_restoreColor();
466    $this->_restoreClip();
467  }
468
469  /**
470   * Note:  _saveClip will  change current  image object,  so  we must
471   * create  all image-dependent  objects after  call to  _saveClip to
472   * ensure resources are created correctly
473   */
474  function save() {
475    $this->_saveClip($this->_clipping[0]['box']);
476    $this->_saveColor($this->_color[0]['rgb']);
477  }
478
479  function setfont($name, $encoding, $size) {
480    $this->_setFont($name, $encoding, $size);
481    return true;
482  }
483
484  function setlinewidth($x) {
485    $dummy = 0;
486    $this->_fixSizes($x, $dummy);
487    imagesetthickness($this->_image, $x);
488  }
489
490  function setrgbcolor($r, $g, $b)  {
491    $color = array('rgb'    => array($r, $g, $b),
492                   'object' => imagecolorallocate($this->_image, $r*255, $g*255, $b*255));
493    $this->_setColor($color);
494  }
495
496  function set_watermark($text) { }
497
498  function show_xy($text, $x, $y) {
499    $this->_fixCoords($x, $y);
500
501    $font = $this->_getFont();
502    $converter = Converter::create();
503
504    global $g_font_resolver_pdf;
505    $fontFile = $g_font_resolver_pdf->ttf_mappings[$font['font']];
506
507    $fontSize = $font['size'];
508
509    $dummy = 0;
510    $this->_fixSizes($dummy, $fontSize);
511
512    $utf8_string = $converter->to_utf8($text, $font['encoding']);
513
514    imagefttext($this->_image,
515                $fontSize * $font['ascender'],
516                0,
517                $x,
518                $y,
519                $this->_getCurrentColor(),
520                TTF_FONTS_REPOSITORY.$fontFile,
521                $utf8_string);
522  }
523
524  /**
525   * Note: the koefficient is just a magic number; I'll need to examine the
526   * imagefttext behavior more closely
527   */
528  function stringwidth($string, $name, $encoding, $size) {
529    $font = $this->_font_factory->getTrueType($name, $encoding);
530    return Font::points($size, $font->stringwidth($string))*1.25;
531  }
532
533  function stroke() {
534    $this->_path->stroke($this->_transform, $this->_image, $this->_getCurrentColor());
535    $this->_path = new Path;
536  }
537}
538?>