File/Convert.php
[pear] / File / Convert.php
1 <?php
2 /**
3  * our moto... "To Convert and Serve"!!!
4  * usage:
5  * 
6  * $x=  new File_Convert("filename", "application/pdf");
7  * $fn = $x->convert("image/jpeg", 200, 0);
8  * $x->serve('inline');
9  * 
10  * generic serve
11  * $x=  new File_Convert("filename", "application/pdf");
12  * $fn = $x->convert("application/pdf"); // does no conversion (as it's the same.
13  * $x->serve('inline'); // can fix IE Mess...
14  * 
15  * options 
16  * {
17  *   delete_all : delete all the generated files after script execution when we call convert()
18  * }
19  * 
20  */
21 /*
22 // test: 
23 echo '<PRE>';
24 $x = new File_Convert(false);
25 print_r($x->getConvMethods('application/msword', 'application/pdf'));
26 print_r($x->getConvMethods('application/msword', 'image/jpeg'));
27 print_r($x->getConvMethods('application/acad', 'image/jpeg'));
28 var_dump($x->getConvMethods('application/acad', 'application/msword')); // impossible
29
30 $x = new File_Convert(file, 'app../excel',array('sheet'=>array(0,1,2...) ));
31 $out = $x->convert('text/csv');
32 */
33
34 class File_Convert
35 {
36    
37     
38     static $options = array();
39     
40     var $fn = ''; // filename
41     var $mimetype = '';
42     // for results..
43     var $debug = false; // set to true to turn on deubgging
44     var $to;
45     var $target;
46     var $lastaction = false;
47     var $log  = array();
48     var $solutions = array();
49     
50     function __construct($fn, $mimetype, $options=array())
51     {
52         var_dump($options);
53         die('option');
54         $this->fn = $fn;
55         
56         if (!file_exists($fn)) {
57             throw new Exception("Source file does not exist:". $fn );
58         }
59         
60         $this->mimetype = $mimetype;
61         self::$options = $options;
62         if (isset($options['debug'])) {
63             $this->debug = $options['debug'];
64         }
65     }
66     
67     
68     
69     /**
70      * check if conversion exists, and return the filename if it does.
71      *
72      */
73     
74     function convertExists($toMimetype, $x= 0, $y =0) 
75     {
76         static $actions = array();
77         $fn = $this->fn;
78         if ($toMimetype != $this->mimetype) {
79             
80             if (!isset($actions["{$this->mimetype} => $toMimetype"])) {
81                 $actions["{$this->mimetype} => $toMimetype"] = $this->getConvMethods($this->mimetype, $toMimetype);;
82             }
83                 
84             $action = $actions["{$this->mimetype} => $toMimetype"];
85             
86            
87             // echo '<PRE>';print_r($action);
88             if (!$action) {
89                 return false;
90             }
91             if (!file_exists($this->fn)) {
92                 return false;
93             }
94             
95             $fn = $action->convertExists($this->fn, $x, $y);
96             
97         }
98        
99         if (!$fn) {
100             return false;
101         }
102         
103         if (!preg_match('#^image/#', $toMimetype) || ( empty($x) && empty($y))) {
104             return $fn;
105         }
106         
107         //echo "testing scale image";
108         require_once 'File/Convert/Solution/scaleimage.php';
109         $sc = new File_Convert_Solution_scaleimage($toMimetype, $toMimetype);
110         //$sc->convert = $this;
111         $sc->debug= $this->debug;
112         $this->solutions[] = $sc;
113             
114         if (strpos($x, 'x')) {
115             $bits = explode('x', $x);
116             $x = (int)$bits[0];
117             $y = empty($bits[1]) ?  0 : (int)$bits[1];;
118         }
119           
120         if (!file_Exists($fn)) {
121             return false;
122         }
123         $fn = $sc->convertExists($fn, (int)$x, (int)$y);
124              
125         
126         //$this->target = $fn;
127         //$this->to = $toMimetype;
128         return $fn;
129     }
130     /**
131      *
132      * actually run the convertion routine.
133      * 
134      */
135     
136     function convert($toMimetype, $x= 0, $y =0, $pg=false) 
137     {
138         //print_R(func_get_args());
139         if ($toMimetype == 'image/jpg') {
140             $toMimetype = 'image/jpeg';
141         }
142         
143         $pg = (int) $pg;
144          if(empty($pg) || is_nan($pg * 1)){
145             $pg = false;
146         }
147         $fn = $this->fn;
148          //echo '<PRE>'; print_r(array('convert', func_get_args()));
149         if (
150                 $toMimetype != $this->mimetype ||
151                 (
152                         $toMimetype == $this->mimetype &&
153                         $toMimetype == 'image/gif'
154                 )
155         ) {
156
157             $action = $this->getConvMethods($this->mimetype, $toMimetype);
158              
159             //echo '<PRE>';print_r($action);
160             if (!$action) {
161                 
162                 $this->debug("No methods found to convert {$this->mimetype} to {$toMimetype}");
163                 return false;
164             }
165             $action->debug = $this->debug;
166             $fn = $action->runconvert($this->fn, $x, $y, $pg);
167             // delete the generated files after script execution
168             if(!empty(self::$options['delete_all'])) {
169                 $this->deleteOnExitAdd($fn);
170             }
171
172             if (!$fn) {
173                 $this->to = $toMimetype;
174                 $this->lastaction = $action->last ? $action->last : $action; // what failed.
175                 return false;
176             }
177             
178             // let's assume that conversions can handle scaling??
179             
180             
181         }  
182              
183
184         if (preg_match('#^image/#', $toMimetype) && $toMimetype != 'image/gif' && ( !empty($x) || !empty($y))) {
185             //var_dump(array($toMimetype));
186                
187             require_once 'File/Convert/Solution.php';
188             $scf = (strpos($x, 'c')  !== false ? 'scaleimagec' : 'scaleimage' );
189             require_once 'File/Convert/Solution/'. $scf . '.php';
190             $scls = 'File_Convert_Solution_' . $scf;
191                 
192             $sc = new $scls($toMimetype, $toMimetype);
193             $sc->debug=  $this->debug;
194             $this->solutions[] = $sc;
195             $x  = str_replace('c', 'x', $x);
196             
197             if (strpos($x, 'x') !== false ) {
198                 $bits = explode('x', $x);
199                 $x = $bits[0];
200                 $y = !is_numeric($bits[1]) ?  '' : (int)$bits[1];
201             }
202             $x = strlen($x) ? (int) $x : '';
203             $y = strlen($y) ? (int) $y : '';
204             //print_r($x); print_r(' > '); print_r($y);exit;
205             
206             $fn = $sc->runconvert($fn,  $x, $y, $pg);
207
208             // delete the generated files after script execution
209             if(!empty(self::$options['delete_all'])) {
210                 $this->deleteOnExitAdd($fn);
211             }
212           
213         }
214 //        print_r($this->target);
215         $this->target = $fn;
216         $this->to = $toMimetype;
217         return $fn;
218         
219         
220     }
221     
222     function serveOnly($type=false, $filename =false, $delete_after = false)
223     {
224         $this->target = $this->fn;
225         $this->to = $this->mimetype;
226         $this->serve($type, $filename , $delete_after );
227     }
228     
229     
230     /**
231      * Serve the file to a browser so it can be downloaded, or viewed.
232      *
233      * @param type string      attachment or inline..
234      * @param filename string  name of file
235      * @param delete_after boolean (false)   delete file after sending..
236      *
237      */
238     function serve($type=false, $filename =false, $delete_after = false) // may die **/
239     {
240         if (empty($this->target)) {
241             // broken image? for images...
242             $cmd = isset($this->lastaction->cmd) ? $this->lastaction->cmd : "No Method";
243             die("not available in this format was: {$this->mimetype}, request: {$this->to}<BR>
244                 Running - $cmd\n" . print_r(is_object($this->lastaction) ? $this->lastaction->log : '',true));
245         }
246         clearstatcache();
247         if (!file_exists($this->target))
248         {
249             trigger_error("Target does not exist: {$this->target}");
250             print_r($this->target);
251             die("file missing");
252        }
253        
254         
255         $fn = $this->target;
256
257         $isIE = preg_match('#msie [0-9.]+#i', isset($_SERVER['HTTP_USER_AGENT']) ? isset($_SERVER['HTTP_USER_AGENT'])  : '');
258         
259         
260         
261         $ts = filemtime($fn);
262         
263         $etag = md5($ts. '!' . $fn);
264         
265         $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ?
266                 trim($_SERVER['HTTP_IF_NONE_MATCH'],"'\"") : false;
267
268         $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? 
269             stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']) : false;
270         
271         $ts_string = gmdate("D, d M Y H:i:s",  $ts) . " GMT";
272         
273         if ((($if_none_match && $if_none_match == $etag) || (!$if_none_match)) &&
274             ($if_modified_since && $if_modified_since == $ts_string))
275         {
276             header('HTTP/1.1 304 Not Modified');
277             exit();
278         }
279         
280         
281         //if (empty($_REQUEST['ts']) && !$isIE && $ifModifiedSince && strtotime($ifModifiedSince) >= $ts) {
282         //    header('HTTP/1.0 304 Not Modified');
283         //    exit; // stop processing
284        // }
285         // ie kludge cause it's brain dead..
286         
287        // var_dump($isIE); 
288         $mt =  $this->to;
289         if ($isIE && preg_match('#^application/#i', $this->to)) {
290             // pdfs' break if we add this line?
291             //$mt = 'application/octet-stream' ;
292             $type = $type === false ?  'attachment' : $type;
293         }
294         $type = $type === false ?  'inline' : $type;
295         
296         
297         
298        
299         //if (!preg_match('#^image\/#i', $this->to)) {
300         $expires = isset(self::$options['expires']) ? self::$options['expires'] : "NOW + 5 MINUTES";
301         // a reasonable expiry time - 5 minutes..
302         header("Expires: ". gmdate("D, d M Y H:i:s",  strtotime($expires)) . " GMT");
303         header("Cache-Control: must-revalidate");
304         header("Pragma: public");     
305         header("Last-Modified: " . $ts_string . " GMT");
306         header("ETag: \"{$etag}\"");
307         
308         //var_dump($mt);
309         require_once 'File/MimeType.php';
310         $fmt = new File_MimeType();
311         $ext = $fmt->toExt($mt);
312         $sfn = basename($fn);
313         $sfn = preg_match('#\.'.$ext.'$#', $sfn) ? $sfn : $sfn. '.' .$ext;
314         //var_dump($sfn);
315         
316         if (empty($filename)) {
317             $filename = $sfn;
318         }
319
320         $filename = preg_replace('/[^\x00-\x7E]/', '', $filename);
321         
322         header('Content-length: '. filesize($fn));
323        // if ($type != 'inline') {
324             header('Content-Disposition: '.$type.'; filename="' . htmlspecialchars($filename).  '"');
325        // }
326        
327         // needs to be removed after debugging - otherwise it logs to error.log
328         //ini_set('display_errors', 0); //trigger_error("Serving: {$this->target} ". filesize($fn));
329         if ($_SERVER["REQUEST_METHOD"] == 'HEAD') {
330             //fclose($fh);
331             exit;
332         }
333         //var_dump($fn, $mt); exit;
334         header('Content-type: '. $mt);
335         
336         // even though we have done a file_exists above - it still errors out here occausionally.
337         $fh = @fopen($fn, 'rb');
338         //fpassthru($fh);
339         
340         // passthrough seems to have problems -- trying fread
341         while($fh && !feof($fh))
342         {
343             echo @fread($fh, 1024*8);
344             @ob_flush();
345             flush();
346         }
347         
348         if ($fh) {
349             fclose($fh);
350         }
351         
352         if ($delete_after) {
353             @unlink($fn);
354         }
355         exit;
356         
357         
358     }
359     /**
360      * 
361      * returned format:
362      *
363      * (
364           from =>
365           to =>
366           cls => instance of class
367      )
368      * 
369      *
370      */
371     
372     function methods()
373     {
374         static $methods = false;
375         if ($methods !== false ) {
376             return $methods;
377         }
378         $methods = array();
379         $base = __DIR__.'/Convert/Solution';
380         $dh = opendir($base);
381         while (false !== ($fn = readdir($dh))) {
382             if (substr($fn,0,1) == '.' ) {
383                 continue;
384             }
385             require_once 'File/Convert/Solution/' . $fn;
386             $cls = 'File_Convert_Solution_'. str_replace('.php', '',$fn);
387             
388             $ref = new ReflectionClass($cls);        
389             $val = $ref->getStaticPropertyValue('rules');
390             
391             foreach($val as $r) {
392                 $r['cls'] = $cls;
393                 $methods[] = $r;
394             }
395             
396             
397         }
398         return $methods;
399         
400         
401         
402     }
403     
404     
405     
406  
407     /**
408      * This recursively calls to find the best match.
409      * First by matching the 'from'
410      *
411      * Then if multiple outputs are available,
412      * It will see if any of those can be used to generate the to, by recurivly calling it'self..
413      *
414      */
415      
416     function getConvMethods($from, $to, $stack = array())
417     {
418             // these source types have to use unoconv....
419         //print_r(array('getConvMethods', func_get_args()));
420         // $pos[converter] => array( list of targets);
421         require_once 'File/Convert/Solution.php';
422
423         if (count($stack) > 4) { // too deepp.. pos. recursion.
424             return false;
425         }
426         $pos = array();
427         // print_r(self::$methods);
428         foreach($this->methods() as $t) {
429             if (!in_array($from, $t['from'])) {
430                 continue;
431             }
432             if (in_array($to,$t['to'])) {
433                 $cls = $t['cls'];
434                 $ret =  new $cls($from, $to);  // found a solid match - returns the method.
435                 //$ret->convert = $this; // recursion?
436                 $this->solutions[] = $ret;
437
438                 //echo "got match?";
439                 return $ret;
440             }
441             // from matches..
442             $pos[$t['cls']] = $t['to']; // list of targets
443             
444         }
445         //echo "got here?";
446         
447         $stack[] = $from;
448         $res = array();
449         foreach($pos as $conv => $ar) {
450             // array contains a list of pos. mimetypes.
451             // 
452             foreach($ar as $targ) {
453                 if ($from == $targ) {
454                     continue; // skip going back...
455                 }
456                 if (in_array($targ, $stack)) {
457                     continue; // going backwards..
458                 }
459                 // we need to build a list here. 
460                 
461                 
462                 
463                 $ns = $stack;
464                 $ns[] = $targ;
465                 $try = $this->getConvMethods($targ, $to, $ns);
466                 // try will be an array of method, from, to (or false)
467                 
468                 if ($try === false) {
469                     continue; // mo way to convert
470                 }
471 //                print_r($conv);exit;
472
473                 $first = new $conv($from, $targ);
474                 //$first->convert = $this;
475                 $sol_list= $first->add($try);
476                 
477                 $res[] = $sol_list;
478                 
479             }
480             
481         }
482         if (empty($res)) {
483              $this->debug("No methods found to convert {$from} to {$to}");
484
485             return false;
486         }
487         // find the shortest..
488         usort  ( $res  , array($this, 'solutionSort'));
489         $best = $res[0];
490         $this->solutions[] = $best;
491         return $best;
492         
493         
494     }
495     function solutionSort($a, $b) {
496         if ($a->count() == $b->count()) {
497             return 0;
498         }
499         return $a->count() < $b->count() ?  -1 : 1;
500     }
501     
502     function debug($str)
503     { 
504         if ($this->debug) {
505             
506             if (is_callable($this->debug)) {
507                 call_user_func($this->debug,$str);
508             } else {
509                 echo $str . "<br/>\n";
510             }
511         }
512         $this->log[] = $str;
513     }
514     
515     static $deleteOnExit = false;
516     /**
517      * generate a tempory file with an extension (dont forget to delete it)
518      */
519     
520     function deleteOnExitAdd($name)
521     {
522         if (self::$deleteOnExit === false) {
523             register_shutdown_function(array('File_Convert','deleteOnExit'));
524             self::$deleteOnExit  = array();
525         }
526         self::$deleteOnExit[] = $name;
527     }
528     
529     static function deleteOnExit()
530     {
531         
532         foreach(self::$deleteOnExit as $fn) {
533             if (file_exists($fn)) {
534                 unlink($fn);
535             }
536         }
537     }
538                 
539           
540            
541 }
542
543
544
545
546
547