<?php
require_once(dirname(__FILE__).'/_plugin.php');

class Pdf_Plugin extends Plugin
{
	/**
	* The full path to the program to extract the text
	*/
	var $binary_path = '';

	/**
	* @var string The name of the program to run to extract the text
	*/
	var $binary = '';

	/**
	* Pick which parser to use and then parse the content using it
	*
	* @param string $content the page content to parse
	*
	* @param string $url the url of the page (for relative link calculation)
	*
	* @return string The extracted text content
	*
	*/
	function Parse($content, $url)
	{
		$this->ExternalSetup();
		if ($this->ExternalCheck()) {
			return $this->ExternalParse($content, $url);
		} elseif ($this->InternalCheck()) {
			return $this->InternalParse($content, $url);
		} else {
			return false;
		}
	}

	function Check()
	{
		$this->ExternalSetup();
		return ($this->ExternalCheck() || $this->InternalCheck());
	}

	function ExternalSetup()
	{
		if (substr(strtolower(PHP_OS), 0, 3) != 'win') {
			$this->binary = 'pdftohtml';
		} else {
			$this->binary = 'pdftohtml.exe';
		}
		$this->DetectPath();
	}

	function DetectPath()
	{
		$this->binary_path = Which($this->binary);
	}

	function InternalCheck()
	{
		$required_functions = array (
			'gzuncompress',
		);

		static $disabled_functions = null;

		if ($disabled_functions === null) {
			$disabled_functions = explode(',', str_replace(' ', '', ini_get('disabled_functions')));
		}


		foreach ($required_functions as $f) {
			if (!function_exists($f)) {
				$this->error = sprintf(GetLang('RequiredFunctionMissing'), $f);
				return false;
			}

			if (in_array($f, $disabled_functions)) {
				$this->error = sprintf(GetLang('RequiredFunctionDisabled'), $f);
				return false;
			}
		}


	}

	function ExternalCheck()
	{
		$required_functions = array (
			'passthru',
			'unlink',
			'tempnam',
		);

		static $disabled_functions = null;

		if ($disabled_functions === null) {
			$disabled_functions = explode(',', str_replace(' ', '', ini_get('disabled_functions')));
		}

		foreach ($required_functions as $f) {
			if (!function_exists($f)) {
				$this->error = sprintf(GetLang('RequiredFunctionMissing'), $f);
				return false;
			}

			if (in_array($f, $disabled_functions)) {
				$this->error = sprintf(GetLang('RequiredFunctionDisabled'), $f);
				return false;
			}
		}

		if ($this->binary_path == '') {
			$this->error = sprintf(GetLang('RequiredBinaryMissing'), $this->binary);
			return false;
		}

		return true;
	}


	function ExternalParse($page, $url)
	{
		$this->DetectPath();

		// We need to save the file to disk first
		$tmpfname = tempnam(TEMP_DIRECTORY, "PDF");
		if ($tmpfname === false) {
			return '';
		}
		$save_ok = file_put_contents($tmpfname, $page);

		// Read the actual content
		ob_start();
		$command = $this->binary_path . ' -q -i -noframes -stdout -- '.escapeshellarg($tmpfname);
		$return_code = null;
		passthru($command, $return_code);
		$htmlstring = ob_get_contents();
		ob_end_clean();

		// Remove our temporary file after we are done with it
		unlink($tmpfname);

		$matches = array();

		if (preg_match('%<title>(.*?)</title>%si', $htmlstring, $matches) && isset($matches[1])) {
			$this->title = $matches[1];
		} else {
			$this->title = basename($url);
		}

		// Remove extra whitespace
		$htmlstring = strip_tags($htmlstring);
		$htmlstring = preg_replace('%\r%', "\n", $htmlstring);
		$htmlstring = preg_replace('%(\s)\s+%s', '\1', $htmlstring);
		$htmlstring = trim($htmlstring);

		return $htmlstring;
	}

	function InternalParse($content, $url)
	{
		$searchstart = 'stream';
		$searchend = 'endstream';
		$pdfText = '';
		$pos = 0;
		$pos2 = 0;
		$startpos = 0;
		$lastendpos = 0;
		$i=0;

		while ($pos !== false && $pos2 !== false) {

			$pos = strpos($content, $searchstart, $startpos);
			$pos2 = strpos($content, $searchend, $startpos + 1);

			$dict_matches = array();

			if ($pos !== false && $pos2 !== false) {

				if ($content[$pos] == 0x0d && $content[$pos + 1] == 0x0a) {
					$pos += 2;
				} else if ($content[$pos] == 0x0a) {
					$pos++;
				}

				if ($content[$pos2 - 2] == 0x0d && $content[$pos2 - 1] == 0x0a) {
					$pos2 -= 2;
				} else if ($content[$pos2 - 1] == 0x0a) {
					$pos2--;
				}

				$textsection = substr($content, $pos + strlen($searchstart) + 2, $pos2 - $pos - strlen($searchstart) - 1);

				//echo "startpos: $startpos\n";
				//echo "len: ".($pos - $startpos)."\n";

				// Get the dictionary of the stream
				$temp_string = substr($content, $startpos, ($pos - $startpos));
				$temp_string = str_replace("\r", ' ', $temp_string);
				$temp_string = strrev($temp_string);
				preg_match('%>>(.*?)<<%s', $temp_string, $dict_matches);
				if (isset($dict_matches[1])) {
					$filters = $this->GetFilters(strrev($dict_matches[1]));
				} else {
					$filters = array();
				}

				while (!empty($filters)) {
					$filter = array_shift($filters);
					switch ($filter) {
						case 'FlateDecode':
							$data = @gzuncompress($textsection);
							break;
						case 'LZWDecode':
							include_once(dirname(__FILE__).'/_lzwdecode.php');
							$LZW = new LZWDecode();
							$data = $LZW->decode($textsection);
							break;
						case 'ASCII85Decode':
							return false;
							$data = $this->ASCII85Decode($textsection);
							break;
						case 'DCTDecode':
							return false;
							$data = $this->DCTDecode($textsection);
							break;
						default:
							$data = '';
							error_log("Unknown filter: $filter\n");
							error_log(print_r(debug_backtrace(), true));
					}
				}

				/*
				DEBUG: Save the uncompressed output to disk for checking out later
				$output_dir = '/home/websites/search/output';
				$filename = basename($url).'.txt';
				$fp = fopen($output_dir.'/'.$filename, "ab");
				if ($fp) {
					fwrite($fp, $data, strlen($data));
					fclose($fp);
				}
				*/

				if ($data !== false) {
					$pdfText .= $this->pdfExtractText($data);
				}

				$startpos = $pos2 + strlen($searchend) - 1;
			}
		}

		$matches = array();
		// If we are debugging on the cli then we need to replace the \r
		// otherwise it will look like the string only contains part of the line
		//$content = str_replace("\r", "", $content);
		preg_match_all('%trailer[\s\r]*<<(.*?)>>%s', $content, $matches);
		$trailer = $matches[1];
		$metadata = $this->GetMetaData($trailer);

		return preg_replace('/(\s)+/', ' ', $pdfText);
	}

	function pdfExtractText($data)
	{
		$matches = array();
		$output = '';

		preg_match_all('%\bBT\b(.*?)\bET\b%s', $data, $matches);

		// If we don't find any text objects there is no text we can extract
		if (!isset($matches[1]) || empty($matches[1])) {
			return '';
		}

		$text_objs = $matches[1];

		foreach ($text_objs as $text_obj) {
			$output .= $this->ParseTextObj($text_obj);
		}

		return preg_replace('/(\s)+/', ' ', $output);
	}

	function ParseTextObj($obj)
	{
		// Initialise the text matrix
		$text_matrix = array(
			array (
				1, 0, 0
			),
			array (
				0, 1, 0
			),
			array (
				0, 0, 1
			),
		);
		$text_line_matrix = $text_matrix;

		$output = '';
		$tlm = 0;
		$tx = 0;
		$ty = 0;
		$text_state = Array (
			'charSpace' => 0,
			'wordSpace' => 0,
		);

		foreach (explode("\n", $obj) as $line_num => $line) {
			$operator = array_pop(preg_split('%[q \]\)]%', $line, -1, PREG_SPLIT_NO_EMPTY));

			switch ($operator) {
				case 'Td':
				{
					// Operands Tx Ty
					// Move to the start of the next line offset by Tx and Ty
					list($tx, $ty) = preg_split('%\s+%s', $line, 2);
					if ($ty > 0 && $tx > 100) {
						$output .= ' ';
					} elseif ($ty > 0) {
						$output .= "\n";
					}
					break;
				}
				case 'TD':
				{
					// Operands Tx Ty
					// Move to the start of the next line offset by Tx and Ty
					// Also set the text leading
					list($tx, $ty) = preg_split('%\s+%s', $line, 2);
					if ($ty > 0 && $tx > 100) {
						$output .= ' ';
					} elseif ($ty > 0) {
						$output .= "\n";
					}
					break;
				}
				case 'Tm':
				{
					// Operands a b c d e f
					// Set the text matrix and the text line matrix
					list($a, $b, $c, $d, $e, $f) = preg_split('%\s+%s', $line);
					if ($f > $text_matrix[3][2]) {
						$output .= "\n";
					}
					$text_matrix = Array(
						Array (
							$a, $b, 0
						),
						Array (
							$c, $d, 0
						),
						Array (
							$e, $f, 1
						),
					);
					$text_line_matrix = $text_matrix;
					/*
					echo "TM at line # $line_num is: ";
					print_r($text_matrix);
					*/
					$text_state['wordSpace'] = $a+$c;
					break;
				}
				case 'T*':
				{
					// Operands -
					// Move to the start of the next line
					$output .= "\n";
					break;
				}
				case 'Tj':
				{
					// Operands string
					$output .= $this->PDFParseString(substr($line, 0, -2));
					break;
				}
				case 'Tf':
				{
					// Operands ?
					// Set a new font
					break;
				}
				case "'":
				{
					// Operands string
					// Move to the next line and show a text string
					$output .= "\n".substr($line, 0, -1);
					break;
				}
				case '"':
				{
					// Operands aw ac string
					// Move to the next line and show a word with aw as word
					// spacing and ac as character spacing
					$output .= "\n".substr($line, 0, -1);
					break;
				}
				case 'TJ':
				{
					// Operand array
					$output .= $this->PDFParseArray(substr($line, 1, -3), $text_state);
					break;
				}
				case 'Tc':
				{
					// Set the character spacing
					list($text_state['charSpace'], $null) = preg_split('%\s+%s', $line, 2);
					break;
				}
				case 'Tw':
				{
					// Set the word spacing
					list($text_state['wordSpace'], $null) = preg_split('%\s+%s', $line, 2);
					break;
				}
				case 'scn':
				case 'g':
				case 'gs':
				{
					// Unhandled ops
					break;
				}
				default:
				{
					/*
					$output .= "OP: $operator\n";
					$output .= "LINE: $line\n";
					*/
				}
			}

		}

		return $output;

	}

	function PDFParseString($strobj)
	{
		$len = strlen($strobj);
		$depth=0;
		$str = '';
		$buffer = '';
		$in_string = false;

		for ($i = 0; $i < $len; $i++) {
			switch ($strobj{$i}) {
				case '(':
				{
					if (!$in_string) {
						$buffer = '';
						$in_string = true;
					}

					$depth++;
					break;
				}
				case ')':
				{
					$depth--;
					if ($depth > 0) {
						$buffer .= ')';
					} else {
						$in_string = false;
						$str .= $buffer;
						$buffer .= '';
					}
					break;
				}
				case '\\':
				{
					$str .= $strobj{$i+1};
					$i++;
					break;
				}
				default:
				{
					$str .= $strobj{$i};
				}
			}
		}
		return $str;
	}

	function PDFParseArray($arr, $text_state)
	{
		$len = strlen($arr);
		$depth=0;
		$offset='';
		$str = '';
		$in_string = false;

		for ($i = 0; $i < $len; $i++) {
			switch ($arr{$i}) {
				case '(':
				{
					//echo "OFFSET: $offset\n";

					if (!$in_string) {
						$buffer = '';
						$in_string = true;
					}

					if (!empty($offset)) {
						// If the offset is bigger than a word gap then add in
						// a space character
						//echo "OFFSET: $offset\n";
						/*
						$buffer .= '-CHARSPACE:'. $text_state['charSpace'].'-';
						$buffer .= '-WORDSPACE:'. $text_state['wordSpace'].'-';
						$buffer .= '-OFFSET:'. $offset.'-';
						$buffer .= '-ABSOFFSET:'. abs($offset).'-';
						*/
						if (abs($offset)  > 100) {
							//echo "DEBUG: Inserting a space since $offset is > than ".$text_state['wordSpace']."\n";
							$buffer .= ' ';
						}
						$offset = '';
					}

					$depth++;
					break;
				}
				case ')':
				{
					$depth--;
					if ($depth > 0) {
						$buffer .= ')';
					} else {
						$in_string = false;
						$str .= $buffer;
						$buffer .= '';
					}
					break;
				}
				case '[':
				case ']':
				{
					break;
				}
				case '\\':
				{
					$buffer .= $arr{$i+1};
					$i++;
					break;
				}
				case '0':
				case '1':
				case '2':
				case '3':
				case '4':
				case '5':
				case '6':
				case '7':
				case '8':
				case '9':
				case '-':
				case '.':
				{
					if ($in_string) {
						$buffer .= $arr{$i};
					} else {
						$offset .= $arr{$i};
					}
					break;
				}
				default:
				{
					$buffer .= $arr{$i};
				}
			}
		}
		return $str;
	}

	function PDFParseDictionary($str)
	{
		$dict = Array();
		$matches = Array();
		preg_match_all("%/(\S+)\s*([^/]+\r)%", $str, $matches);
		foreach (array_keys($matches[0]) as $key) {
			$dict[$matches[1][$key]] = trim($matches[2][$key]);
		}
		return $dict;
	}

	function GetMetaData($trailers)
	{
		if (is_array($trailers)) {
			foreach ($trailers as $trailer) {
				if (strpos($trailer, '/Info') !== false) {
					$dict = $this->PDFParseDictionary($trailer);
				}
			}
		} else {
			$dict = $this->PDFParseDictionary($trailers);
		}

		// Double check the Info value is a reference to an object
		if (substr($dict['Info'], -1) == 'R') {

		}
	}

	// {{{ GetFilters($dictstr)
	function GetFilters($dictstr)
	{
		$dictstr = trim($dictstr);

		$dict = array();

		$filterToken = "/Filter";

		$startKey = strpos($dictstr, $filterToken);
		if ($startKey === false) {
			return array();
		}

		$startVal = $startKey + strlen($filterToken) +1;


		$endVal = strlen($dictstr);
		foreach (array(' ', '/', '\t', '\r', '\n', '[', '<', '>') as $delim) {
			$pos = strpos($dictstr, $delim, $startVal +1);
			if ($pos !== false && $pos < $endVal) {
				$endVal = $pos;
			}
		}

		if ($endVal === false) {
			$endVal = strlen($dictstr);
		}
		$firstChar = substr($dictstr, $startVal, 1);

		if ($firstChar == '/') {
			return array(substr($dictstr, $startVal+1, $endVal-$startVal-1));
		} elseif ($firstChar == '[') {
		} else {
			//echo $firstChar."\n";
		}

		return $dict;
	}
	// }}}

	// {{{ ASCII85Decode($in)
	function ASCII85Decode($in)
	{
		$out = "";
		$state = 0;
		$chn = null;

		$l = strlen($in);

		for ($k = 0; $k < $l; ++$k) {
			$ch = ord($in[$k]) & 0xff;

			if ($ch == ord('~')) {
				break;
			}
			if (preg_match('/^\s$/',chr($ch))) {
				continue;
			}
			if ($ch == ord('z') && $state == 0) {
				$out .= chr(0).chr(0).chr(0).chr(0);
				continue;
			}
			if ($ch < ord('!') || $ch > ord('u')) {
				$this->fpdi->error("Illegal character in ASCII85Decode.");
			}

			$chn[$state++] = $ch - ord('!');

			if ($state == 5) {
				$state = 0;
				$r = 0;
				for ($j = 0; $j < 5; ++$j) {
					$r = $r * 85 + $chn[$j];
				}
				$out .= chr($r >> 24);
				$out .= chr($r >> 16);
				$out .= chr($r >> 8);
				$out .= chr($r);
			}
		}
		$r = 0;

		if ($state == 1) {
			$this->fpdi->error("Illegal length in ASCII85Decode.");
		}
		if ($state == 2) {
			$r = $chn[0] * 85 * 85 * 85 * 85 + ($chn[1]+1) * 85 * 85 * 85;
			$out .= chr($r >> 24);
		} else if ($state == 3) {
			$r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85  + ($chn[2]+1) * 85 * 85;
			$out .= chr($r >> 24);
			$out .= chr($r >> 16);
		} else if ($state == 4) {
			$r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85  + $chn[2] * 85 * 85  + ($chn[3]+1) * 85 ;
			$out .= chr($r >> 24);
			$out .= chr($r >> 16);
			$out .= chr($r >> 8);
		}

		return $out;
	}
	// }}}

	// {{{ DCTDecode($text)
	function DCTDecode($text)
	{
		return false;
	}
	//}}}

}

if (!function_exists('file_put_contents')) {
	function file_put_contents($filename, $data)
	{
        $fp = fopen($filename, "w+b");
        if (!$fp) {
            return false;
        }
        $bytes_written = fwrite($fp, $data, strlen($data));
        fclose($fp);
        if ($bytes_written === false) {
            return false;
        } else {
            return $bytes_written;
        }
    }
}


?>
