2 // +---------------------------------------------------------------------+
3 // | WikiPluginCached.php |
4 // +---------------------------------------------------------------------+
5 // | Copyright (C) 2002 Johannes Große (Johannes Große) |
6 // | You may copy this code freely under the conditions of the GPL |
7 // +---------------------------------------------------------------------+
8 // | You should set up the options in plugincache-config.php |
9 // | $ pear install http://pear.php.net/get/Cache |
10 // +---------------------------------------------------------------------+
12 require_once "lib/WikiPlugin.php";
13 require_once "lib/plugincache-config.php";
14 // Try the installed pear class first. It might be newer.
15 @require_once('Cache.php');
16 if (!class_exists('Cache'))
17 require_once('lib/pear/Cache.php'); // You have to create your own copy here
19 define('PLUGIN_CACHED_HTML',0);
20 define('PLUGIN_CACHED_IMG_INLINE',1);
21 define('PLUGIN_CACHED_IMG_ONDEMAND',2);
22 define('PLUGIN_CACHED_MAP',3);
25 * An extension of the WikiPlugin class to allow image output and
27 * There are several abstract functions to be overloaded.
28 * Have a look at the example files
29 * <ul><li>plugin/TexToPng.php</li>
30 * <li>plugin/CacheTest.php (extremely simple example)</li>
31 * <li>plugin/RecentChanges.php</li>
32 * <li>plugin/VisualWiki.php</li></ul>
34 * @author Johannes Große
37 class WikiPluginCached extends WikiPlugin
40 * Produces URL and id number from plugin arguments which later on,
41 * will allow to find a cached image or to reconstruct the complete
42 * plugin call to recreate the image.
44 * @param cache object the cache object used to store the images
45 * @param argarray array all parameters (including those set to
46 * default values) of the plugin call to be
49 * @return array(id,url)
51 function genUrl($cache,$argarray) {
52 $cacheparams = $GLOBALS['CacheParams'];
54 $plugincall = serialize( array(
55 'pluginname' => $this->getName(),
56 'arguments' => $argarray ) );
57 $id = $cache->generateId( $plugincall );
59 $url = $cacheparams['cacheurl']; // FIXME: SERVER_URL ?
60 if (($lastchar = substr($url,-1)) == '/') {
61 $url = substr($url, 0, -1);
63 if (strlen($plugincall)>$cacheparams['maxarglen']) {
64 // we can't send the data as URL so we just send the id
66 if (!$request->getSessionVar('imagecache'.$id)) {
67 $request->setSessionVar('imagecache'.$id, $plugincall);
69 $plugincall = false; // not needed anymore
73 // this indicates that a direct call of the image creation
74 // script is wished ($url is assumed to link to the script)
75 $url .= "id=$id" . ($plugincall ? '&args='.rawurlencode($plugincall) : '');
77 // we are supposed to use the indirect 404 ErrorDocument method
78 // ($url is assumed to be the url of the image in
79 // cache_dir and the image creation script is referred to in the
80 // ErrorDocument 404 directive.)
81 $url .= '/' . $cacheparams['filename_prefix'] . $id . '.img'
82 . ($plugincall ? '?args='.rawurlencode($plugincall) : '');
84 return array($id, $url);
88 * Replaces the abstract run method of WikiPlugin to implement
89 * a cache check which can avoid redundant runs.
90 * <b>Do not override this method in a subclass. Instead you may
91 * rename your run method to getHtml, getImage or getMap.
92 * Have a close look on the arguments and required return values,
96 * @param dbi WikiDB database abstraction class
97 * @param argstr string plugin arguments in the call from PhpWiki
98 * @param request Request ???
99 * @return string HTML output to be printed to browser
105 function run($dbi, $argstr, $request) {
106 $cache = WikiPluginCached::newCache();
107 $cacheparams = $GLOBALS['CacheParams'];
109 $sortedargs = $this->getArgs($argstr, $request);
110 if (is_array($sortedargs) )
113 list($id,$url) = $this->genUrl($cache, $sortedargs);
115 // ---------- html and img gen. -----------------
116 if ($this->getPluginType() == PLUGIN_CACHED_IMG_ONDEMAND) {
117 return $this->embedImg($url, $dbi, $sortedargs, $request);
121 $content = $cache->get($id, 'imagecache');
122 switch($this->getPluginType()) {
123 case PLUGIN_CACHED_HTML:
124 if (!$content || !$content['html']) {
126 $content['html'] = $this->getHtml($dbi,$sortedargs,$request);
127 if ($errortext = $this->getError()) {
128 WikiPluginCached::printError($errortext,'html');
134 case PLUGIN_CACHED_IMG_INLINE:
135 if ($cacheparams['usecache']&&(!$content || !$content['image'])) {
136 $do_save = WikiPluginCached::produceImage($content, $this, $dbi, $sortedargs, $request, 'html');
137 $content['html'] = $do_save?$this->embedImg($url, $dbi, $sortedargs, $request) : false;
140 case PLUGIN_CACHED_MAP:
141 if (!$content || !$content['image'] || !$content['html'] ) {
142 $do_save = WikiPluginCached::produceImage($content, $this, $dbi, $sortedargs, $request, 'html');
143 $content['html'] = $do_save?WikiPluginCached::embedMap($id,
144 $url,$content['html'],$dbi,$sortedargs,$request):false;
149 $expire = $this->getExpire($dbi,$sortedargs,$request);
150 $cache->save($id, $content, $expire,'imagecache');
152 if ($content['html'])
153 return $content['html'];
158 /* --------------------- virtual or abstract functions ----------- */
161 * Sets the type of the plugin to html, image or map
165 * @return int determines the plugin to produce either html,
166 * an image or an image map; uses on of the
167 * following predefined values
169 * <li>PLUGIN_CACHED_HTML</li>
170 * <li>PLUGIN_CACHED_IMG_INLINE</li>
171 * <li>PLUGIN_CACHED_IMG_ONDEMAND</li>
172 * <li>PLUGIN_CACHED_MAP</li>
175 function getPluginType() {
176 return PLUGIN_CACHED_IMG_ONDEMAND;
182 * Creates an image handle from the given user arguments.
183 * This method is only called if the return value of
184 * <code>getPluginType</code> is set to
185 * PLUGIN_CACHED_IMG_INLINE or PLUGIN_CACHED_IMG_ONDEMAND.
187 * @access protected pure virtual
188 * @param dbi WikiDB database abstraction class
189 * @param argarray array complete (!) arguments to produce
190 * image. It is not necessary to call
191 * WikiPlugin->getArgs anymore.
192 * @param request Request ???
193 * @return imagehandle image handle if successful
194 * false if an error occured
196 function getImage($dbi,$argarray,$request) {
197 trigger_error('WikiPluginCached::getImage: pure virtual function in file '
198 . __FILE__ . ' line ' . __LINE__, E_USER_ERROR);
203 * Sets the life time of a cache entry in seconds.
204 * Expired entries are not used anymore.
205 * During a garbage collection each expired entry is
206 * removed. If removing all expired entries is not
207 * sufficient, the expire time is ignored and removing
208 * is determined by the last "touch" of the entry.
210 * @access protected virtual
211 * @param dbi WikiDB database abstraction class
212 * @param argarray array complete (!) arguments. It is
213 * not necessary to call
214 * WikiPlugin->getArgs anymore.
215 * @param request Request ???
216 * @return string format: '+seconds'
219 function getExpire($dbi,$argarray,$request) {
220 return '0'; // persist forever
224 * Decides the image type of an image output.
225 * Always used unless plugin type is PLUGIN_CACHED_HTML.
227 * @access protected virtual
228 * @param dbi WikiDB database abstraction class
229 * @param argarray array complete (!) arguments. It is
230 * not necessary to call
231 * WikiPlugin->getArgs anymore.
232 * @param request Request ???
233 * @return string 'png', 'jpeg' or 'gif'
235 function getImageType($dbi,$argarray,$request) {
236 if (in_array($argarray['imgtype'],$GLOBAL['CacheParams']['imgtypes']))
237 return $argarray['imgtype'];
244 * Produces the alt text for an image.
245 * <code> <img src=... alt="getAlt(...)"> </code>
247 * @access protected virtual
248 * @param dbi WikiDB database abstraction class
249 * @param argarray array complete (!) arguments. It is
250 * not necessary to call
251 * WikiPlugin->getArgs anymore.
252 * @param request Request ???
253 * @return string "alt" description of the image
255 function getAlt($dbi,$argarray,$request) {
256 return '<?plugin '.$this->getName().' '.$this->glueArgs($argarray).'?>';
260 * Creates HTML output to be cached.
261 * This method is only called if the plugin_type is set to
262 * PLUGIN_CACHED_HTML.
264 * @access protected pure virtual
265 * @param dbi WikiDB database abstraction class
266 * @param argarray array complete (!) arguments to produce
267 * image. It is not necessary to call
268 * WikiPlugin->getArgs anymore.
269 * @param request Request ???
270 * @return string html to be printed in place of the plugin command
271 * false if an error occured
273 function getHtml($dbi, $argarray, $request) {
274 trigger_error('WikiPluginCached::getHtml: pure virtual function in file '
275 . __FILE__ . ' line ' . __LINE__, E_USER_ERROR);
280 * Creates HTML output to be cached.
281 * This method is only called if the plugin_type is set to
282 * PLUGIN_CACHED_HTML.
284 * @access protected pure virtual
285 * @param dbi WikiDB database abstraction class
286 * @param argarray array complete (!) arguments to produce
287 * image. It is not necessary to call
288 * WikiPlugin->getArgs anymore.
289 * @param request Request ???
290 * @return array(html,handle) html for the map interior (to be specific,
291 * only <area;> tags defining hot spots)
292 * handle is an imagehandle to the corresponding
294 * array(false,false) if an error occured
296 function getMap($dbi, $argarray, $request) {
297 trigger_error('WikiPluginCached::getHtml: pure virtual function in file '
298 . __FILE__ . ' line ' . __LINE__, E_USER_ERROR);
301 /* --------------------- produce Html ----------------------------- */
304 * Creates an HTML map hyperlinked to the image specified
305 * by url and containing the hotspots given by map.
308 * @param id string unique id for the plugin call
309 * @param url string url pointing to the image part of the map
310 * @param map string <area> tags defining active
312 * @param dbi WikiDB database abstraction class
313 * @param argarray array complete (!) arguments to produce
314 * image. It is not necessary to call
315 * WikiPlugin->getArgs anymore.
316 * @param request Request ???
317 * @return string html output
319 function embedMap($id,$url,$map,$dbi,$argarray,$request) {
320 // id is not unique if the same map is produced twice
321 $key = substr($id,0,8).substr(microtime(),0,6);
322 return HTML(HTML::map(array( 'name' => $key ), $map ),
325 // 'alt' => htmlspecialchars($this->getAlt($dbi,$argarray,$request))
326 'usemap' => '#'.$key ))
331 * Creates an HTML <img> tag hyperlinking to the specified
332 * url and produces an alternative text for non-graphical
336 * @param url string url pointing to the image part of the map
337 * @param map string <area> tags defining active
339 * @param dbi WikiDB database abstraction class
340 * @param argarray array complete (!) arguments to produce
341 * image. It is not necessary to call
342 * WikiPlugin->getArgs anymore.
343 * @param request Request ???
344 * @return string html output
346 function embedImg($url,$dbi,$argarray,$request) {
347 return HTML::img( array(
349 'alt' => htmlspecialchars($this->getAlt($dbi,$argarray,$request)) ) );
353 // --------------------------------------------------------------------------
354 // ---------------------- static member functions ---------------------------
355 // --------------------------------------------------------------------------
358 * Creates one static PEAR Cache object and returns copies afterwards.
359 * FIXME: There should be references returned
361 * @access static protected
362 * @return Cache copy of the cache object
364 function newCache() {
367 $cacheparams = $GLOBALS['CacheParams'];
369 if (!is_object($staticcache)) {
370 /* $pearFinder = new PearFileFinder;
371 $pearFinder->includeOnce('Cache.php');*/
373 $staticcache = new Cache($cacheparams['database'],$cacheparams);
374 $staticcache->gc_maxlifetime = $cacheparams['maxlifetime'];
376 if (!$cacheparams['usecache']) {
377 $staticcache->setCaching(false);
380 return $staticcache; // FIXME: use references ?
384 * Determines whether a needed image type may is available
385 * from the GD library and gives an alternative otherwise.
387 * @access public static
388 * @param wish string one of 'png', 'gif', 'jpeg', 'jpg'
389 * @return string the image type to be used ('png', 'gif', 'jpeg')
390 * 'html' in case of an error
393 function decideImgType($wish) {
394 if ($wish=='html') return $wish;
395 if ($wish=='jpg') { $wish = 'jpeg'; }
397 $supportedtypes = array();
398 // Todo: swf, pdf, ...
405 /* // these do work but not with the ImageType bitmask
412 $presenttypes = ImageTypes();
413 foreach($imagetypes as $imgtype => $bitmask)
414 if ( $presenttypes && $bitmask )
415 array_push($supportedtypes, $imgtype);
417 if (in_array($wish, $supportedtypes))
419 elseif (!empty($supportedtypes))
420 return reset($supportedtypes);
428 * Writes an image into a file or to the browser.
429 * Note that there is no check if the image can
432 * @access public static
433 * @param imgtype string 'png', 'gif' or 'jpeg'
434 * @param imghandle string image handle containing the image
435 * @param imgfile string file name of the image to be produced
437 * @see decideImageType
439 function writeImage($imgtype, $imghandle, $imgfile=false) {
440 if ($imgtype != 'html') {
441 $func = "Image" . strtoupper($imgtype);
443 $func($imghandle,$imgfile);
452 * Sends HTTP Header for some predefined file types.
453 * There is no parameter check.
455 * @access public static
456 * @param doctype string 'gif', 'png', 'jpeg', 'html'
459 function writeHeader($doctype) {
460 $IMAGEHEADER = array(
461 'gif' => 'Content-type: image/gif',
462 'png' => 'Content-type: image/png',
463 'jpeg' => 'Content-type: image/jpeg',
464 'xbm' => 'Content-type: image/xbm',
465 'xpm' => 'Content-type: image/xpm',
466 'gd' => 'Content-type: image/gd',
467 'gd2' => 'Content-type: image/gd2',
468 'wbmp' => 'Content-type: image/vnd.wap.wbmp', // wireless bitmaps for PDA's and such.
469 'html' => 'Content-type: text/html' );
471 Header($IMAGEHEADER[$doctype]);
476 * Converts argument array to a string of format option="value".
477 * This should only be used for displaying plugin options for
478 * the quoting of arguments is not safe, yet.
480 * @access public static
481 * @param argarray array contains all arguments to be converted
482 * @return string concated arguments
484 function glueArgs($argarray) {
485 if (!empty($argarray)) {
487 while (list($key,$value)=each($argarray)) {
488 $argstr .= $key. '=' . '"' . $value . '" ';
489 // FIXME FIXME: How are values quoted? Can a value contain " ?
491 return substr($argstr,0,strlen($argstr)-1);
496 // ---------------------- FetchImageFromCache ------------------------------
499 * Extracts the cache entry id from the url and the plugin call
500 * parameters if available.
502 * @access private static
503 * @param id string return value. Image is stored under this id.
504 * @param plugincall string return value. Only returned if present in url.
505 * Contains all parameters to reconstruct
507 * @param cache Cache PEAR Cache object
508 * @param request Request ???
509 * @param errorformat string format which should be used to
510 * output errors ('html', 'png', 'gif', 'jpeg')
511 * @return boolean false if an error occurs, true otherwise.
512 * Param id and param plugincall are
513 * also return values.
515 function checkCall1(&$id, &$plugincall,$cache,$request, $errorformat) {
516 $id=$request->getArg('id');
517 $plugincall=rawurldecode($request->getArg('args'));
521 // This should never happen, so do not gettextify.
522 $errortext = "Neither 'args' nor 'id' given. Cannot proceed without parameters.";
523 WikiPluginCached::printError($errorformat, $errortext);
526 $id = $cache->generateId( $plugincall );
534 * Extracts the parameters necessary to reconstruct the plugin
535 * call needed to produce the requested image.
537 * @access static private
538 * @param plugincall string reference to serialized array containing both
539 * name and parameters of the plugin call
540 * @param request Request ???
541 * @return boolean false if an error occurs, true otherwise.
544 function checkCall2(&$plugincall,$request) {
545 // if plugincall wasn't sent by URL, it must have been
546 // stored in a session var instead and we can retreive it from there
548 if (!$plugincall=$request->getSessionVar('imagecache'.$id)) {
549 // I think this is the only error which may occur
550 // without having written bad code. So gettextify it.
551 $errortext = sprintf(
552 gettext ("There is no image creation data available to id '%s'. Please reload referring page." ),
554 WikiPluginCached::printError($errorformat, $errortext);
558 $plugincall = unserialize($plugincall);
564 * Creates an image or image map depending on the plugin type.
565 * @access static private
566 * @param content array reference to created array which overwrite the keys
567 * 'image', 'imagetype' and possibly 'html'
568 * @param plugin WikiPluginCached plugin which is called to create image or map
569 * @param dbi WikiDB handle to database
570 * @param argarray array Contains all arguments needed by plugin
571 * @param request Request ????
572 * @param errorformat string outputs errors in 'png', 'gif', 'jpg' or 'html'
573 * @return boolean error status; true=ok; false=error
575 function produceImage(&$content, $plugin, $dbi, $argarray, $request, $errorformat) {
576 $plugin->resetError();
577 $content['html'] = $imagehandle = false;
578 if ($plugin->getPluginType() == PLUGIN_CACHED_MAP ) {
579 list($imagehandle,$content['html']) = $plugin->getMap($dbi, $argarray, $request);
581 $imagehandle = $plugin->getImage($dbi, $argarray, $request);
584 $content['imagetype']
585 = WikiPluginCached::decideImgType($plugin->getImageType($dbi, $argarray, $request));
586 $errortext = $plugin->getError();
588 if (!$imagehandle||$errortext) {
590 $errortext = "'<?plugin ".$plugin->getName(). ' '
591 . WikiPluginCached::glueArgs($argarray)." ?>' returned no image, "
592 . " although no error was reported.";
594 WikiPluginCached::printError($errorformat, $errortext);
598 // image handle -> image data
599 $cacheparams = $GLOBALS['CacheParams'];
600 $tmpfile = tempnam($cacheparams['cache_dir'],$cacheparams['filename_prefix']);
601 WikiPluginCached::writeImage($content['imagetype'], $imagehandle, $tmpfile);
602 ImageDestroy($imagehandle);
603 if (file_exists($tmpfile)) {
604 $fp = fopen($tmpfile,'rb');
605 $content['image'] = fread($fp,filesize($tmpfile));
608 if ($content['image'])
616 * Main function for obtaining images from cache or generating on-the-fly
617 * from parameters sent by url or session vars.
619 * @access static public
620 * @param dbi WikiDB handle to database
621 * @param request Request ???
622 * @param errorformat string outputs errors in 'png', 'gif', 'jpeg' or 'html'
624 function fetchImageFromCache($dbi,$request,$errorformat='png') {
625 $cache = WikiPluginCached::newCache();
626 $errorformat = WikiPluginCached::decideImgType($errorformat);
628 if (!WikiPluginCached::checkCall1($id,$plugincall,$cache,$request,$errorformat)) return false;
631 $content = $cache->get($id,'imagecache');
633 if ($content && $content['image']) {
634 WikiPluginCached::writeHeader($content['imagetype']);
635 print $content['image'];
639 // produce image, now. At first, we need plugincall parameters
640 if (!WikiPluginCached::checkCall2($plugincall,$request)) return false;
642 $pluginname = $plugincall['pluginname'];
643 $argarray = $plugincall['arguments'];
645 $loader = new WikiPluginLoader;
646 $plugin = $loader->getPlugin($pluginname);
648 // cache empty, but image maps have to be created _inline_
649 // so ask user to reload wiki page instead
650 $cacheparams = $GLOBALS['CacheParams'];
651 if (($plugin->getPluginType() == PLUGIN_CACHED_MAP) && $cacheparams['force_syncmap']) {
652 $errortext = gettext('Image map expired. Reload wiki page to recreate its html part.');
653 WikiPluginCached::printError($errorformat, $errortext);
657 if (!WikiPluginCached::produceImage(&$content, $plugin, $dbi, $argarray, $request, $errorformat) ) return false;
659 $expire = $plugin->getExpire($dbi,$argarray,$request);
661 if ($content['image']) {
662 $cache->save($id, $content, $expire,'imagecache');
663 WikiPluginCached::writeHeader($content['imagetype']);
664 print $content['image'];
668 $errortext = "Could not create image file from imagehandle.";
669 WikiPluginCached::printError($errorformat, $errortext);
671 } // FetchImageFromCache
673 // -------------------- error handling ----------------------------
676 * Resets buffer containing all error messages. This is allways
677 * done before invoking any abstract creation routines like
678 * <code>getImage</code>.
683 function resetError() {
684 $this->_errortext = '';
688 * Returns all accumulated error messages.
691 * @return string error messages printed with <code>complain</code>.
693 function getError() {
694 return $this->_errortext;
698 * Collects the error messages in a string for later output
699 * by WikiPluginCached. This should be used for any errors
700 * that occur during data (html,image,map) creation.
703 * @param addtext string errormessage to be printed (separate
704 * multiple lines with '\n')
707 function complain($addtext) {
708 $this->_errortext .= $addtext;
712 * Outputs the error as image if possible or as html text
713 * if wished or html header has already been sent.
715 * @access static protected
716 * @param imgtype string 'png', 'gif', 'jpeg' or 'html'
717 * @param errortext string guess what?
720 function printError($imgtype, $errortext) {
721 $imgtype = WikiPluginCached::decideImgType($imgtype);
723 $talkedallready = ob_get_contents() || headers_sent();
724 if (($imgtype=='html') || $talkedallready) {
725 trigger_error($errortext, E_USER_WARNING);
727 $red = array(255,0,0);
728 $grey = array(221,221,221);
729 $im = WikiPluginCached::text2img($errortext, 2, $red, $grey);
731 trigger_error($errortext, E_USER_WARNING);
734 WikiPluginCached::writeHeader($imgtype);
735 WikiPluginCached::writeImage($imgtype, $im);
742 * Basic text to image converter for error handling which allows
743 * multiple line output.
744 * It will only output the first 25 lines of 80 characters. Both
745 * values may be smaller if the chosen font is to big for there
746 * is further restriction to 600 pixel in width and 350 in height.
748 * @access static public
749 * @param txt string multi line text to be converted
750 * @param fontnr integer number (1-5) telling gd which internal font to use;
751 * I recommend font 2 for errors and 4 for help texts.
752 * @param textcol array text color as a list of the rgb components; array(red,green,blue)
753 * @param bgcol array background color; array(red,green,blue)
754 * @return string image handle for gd routines
756 function text2img($txt,$fontnr,$textcol,$bgcol) {
757 // basic (!) output for error handling
760 if ($fontnr<1 || $fontnr>5) {
763 if (!is_array($textcol) || !is_array($bgcol)) {
764 $textcol = array(0,0,0);
765 $bgcol = array(255,255,255);
767 foreach( array_merge($textcol,$bgcol) as $component) {
768 if ($component<0 || $component > 255) {
769 $textcol = array(0,0,0);
770 $bgcol = array(255,255,255);
775 // prepare Parameters
777 // set maximum values
784 $charx = ImageFontWidth($fontnr);
785 $chary = ImageFontHeight($fontnr);
787 $marginy = floor($chary/2);
789 $IMAGESIZE['cols'] = min($IMAGESIZE['cols'], floor(($IMAGESIZE['width'] - 2*$marginx )/$charx));
790 $IMAGESIZE['rows'] = min($IMAGESIZE['rows'], floor(($IMAGESIZE['height'] - 2*$marginy )/$chary));
797 $npos = strpos($txt, "\n");
800 $breaklen = min($IMAGESIZE['cols'],$len);
802 $breaklen = min($npos+1, $IMAGESIZE['cols']);
804 $lines[$y] = chop(substr($txt, 0, $breaklen));
805 $wx = max($wx,strlen($lines[$y++]));
806 $txt = substr($txt, $breaklen);
807 } while ($txt && ($y < $IMAGESIZE['rows']));
809 // recalculate image size
810 $IMAGESIZE['rows'] = $y;
811 $IMAGESIZE['cols'] = $wx;
813 $IMAGESIZE['width'] = $IMAGESIZE['cols'] * $charx + 2*$marginx;
814 $IMAGESIZE['height'] = $IMAGESIZE['rows'] * $chary + 2*$marginy;
816 // create blank image
817 $im = @ImageCreate($IMAGESIZE['width'],$IMAGESIZE['height']);
819 $col = ImageColorAllocate($im, $textcol[0], $textcol[1], $textcol[2]);
820 $bg = ImageColorAllocate($im, $bgcol[0], $bgcol[1], $bgcol[2]);
822 ImageFilledRectangle($im, 0, 0, $IMAGESIZE['width']-1, $IMAGESIZE['height']-1, $bg);
825 foreach($lines as $nr => $textstr) {
826 ImageString( $im, $fontnr, $marginx, $marginy+$nr*$chary,
832 } // WikiPluginCached
840 // c-hanging-comment-ender-p: nil
841 // indent-tabs-mode: nil