You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
523 lines
15 KiB
PHP
523 lines
15 KiB
PHP
<?php
|
|
|
|
/**
|
|
* rim - Remote Image Library
|
|
*
|
|
* @author Matej Baćo <matejbaco@gmail.com>
|
|
* @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU LESSER GENERAL PUBLIC LICENSE
|
|
*/
|
|
require_once (dirname(__FILE__) . '/libs/ArrayFunctionHelper.php');
|
|
require_once (dirname(__FILE__) . '/libs/CurlMulti.php');
|
|
|
|
class rim {
|
|
public $profile = false;
|
|
|
|
/**
|
|
* getMultiImageTypeAndSize
|
|
*
|
|
* @param array $urls
|
|
* Image urls to fetch, key-es will be preserved
|
|
* @param array $options
|
|
* array(
|
|
* 'max_num_of_threads' => (integer) def: 10, // how many threads to use when fetching data
|
|
* 'time_limit' => (decimal) def: null, // limit total execution time in seconds, 2.34 is 2 seconds and 34 miliseconds
|
|
* 'callback' => (array) def: null, // callback to call after fetch of each image data
|
|
* 'curl_connect_timeout' => (integer) def: 2, // curl therad connect timeout
|
|
* 'curl_timeout' => (integer) def: 3 // curl thread timeout
|
|
* 'curl_buffer_size' => (integer) def: 0 // size of buffer for jpeg images, if 0 will be auto calculated
|
|
* )
|
|
*/
|
|
public function getMultiImageTypeAndSize($urls, $options = array()) {
|
|
if (empty($urls) || !is_array($urls)) {
|
|
throw new Exception('Invalid arguments.');
|
|
}
|
|
|
|
$default_options = array (
|
|
'max_num_of_threads' => 10,
|
|
'time_limit' => null,
|
|
'callback' => null,
|
|
'curl_connect_timeout' => 2,
|
|
'curl_timeout' => 3,
|
|
'curl_buffer_size' => 0
|
|
);
|
|
$options = ArrayFunctionHelper::arrayMerge($default_options, $options);
|
|
|
|
$this->_curlMulti = new CurlMulti();
|
|
$this->_curlMulti->maxThreads = $options['max_num_of_threads'];
|
|
$this->_curlMulti->curlBufferSize = $options['curl_buffer_size'];
|
|
if (!empty($options['time_limit'])) {
|
|
$this->_curlMulti->timeLimit = $options['time_limit'];
|
|
}
|
|
|
|
$this->_curlMulti->defaultCurlThreadOptions = array (
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_SSL_VERIFYHOST => false,
|
|
CURLOPT_RETURNTRANSFER => true, // return body
|
|
CURLOPT_HEADER => false, // return headers
|
|
CURLOPT_BINARYTRANSFER => true, // raw data
|
|
CURLOPT_FOLLOWLOCATION => true, // follow redirects
|
|
CURLOPT_ENCODING => "", // handle all encodings
|
|
CURLOPT_USERAGENT => "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", // who am i
|
|
CURLOPT_AUTOREFERER => true, // set referer on redirect
|
|
CURLOPT_CONNECTTIMEOUT => $options['curl_connect_timeout'], // timeout on connect
|
|
CURLOPT_TIMEOUT => $options['curl_timeout']
|
|
); // timeout on response
|
|
|
|
$images_data = array ();
|
|
foreach ($urls as $key => $url) {
|
|
$images_data[$key] = array (
|
|
'url' => $url,
|
|
'error' => array ()
|
|
);
|
|
|
|
if (!empty($options['callback'])) {
|
|
$images_data[$key]['callback'] = $options['callback'];
|
|
}
|
|
|
|
if ($this->profile) {
|
|
$images_data[$key]['trace'] = array ();
|
|
$images_data[$key]['downloaded_size_trace'] = array ();
|
|
$images_data[$key]['buffer_size_trace'] = array ();
|
|
}
|
|
|
|
$this->_getImageData($images_data[$key]);
|
|
}
|
|
|
|
$this->_curlMulti->process();
|
|
|
|
return $images_data;
|
|
}
|
|
|
|
/**
|
|
* getSingleImageTypeAndSize
|
|
*
|
|
* @param array $url
|
|
* Image url to fetch
|
|
* @param array $options
|
|
* array(
|
|
* 'time_limit' => (decimal) def: null, // limit total execution time in seconds, 2.34 is 2 seconds and 34 miliseconds
|
|
* 'callback' => (array) def: null, // callback to call after fetch of each image data
|
|
* 'curl_connect_timeout' => (integer) def: 2, // curl therad connect timeout
|
|
* 'curl_timeout' => (integer) def: 3 // curl thread timeout
|
|
* 'curl_buffer_size' => (integer) def: 0 // size of buffer for jpeg images, if 0 will be auto calculated
|
|
* )
|
|
*/
|
|
public function getSingleImageTypeAndSize($url, $options = array()) {
|
|
$urls = array (
|
|
$url
|
|
);
|
|
|
|
$default_options = array (
|
|
'time_limit' => null,
|
|
'callback' => null,
|
|
'curl_connect_timeout' => 2,
|
|
'curl_timeout' => 3,
|
|
'curl_buffer_size' => 0
|
|
);
|
|
$options = ArrayFunctionHelper::arrayMerge($default_options, $options);
|
|
|
|
$this->_curlMulti = new CurlMulti();
|
|
$this->_curlMulti->maxThreads = 1;
|
|
$this->_curlMulti->curlBufferSize = $options['curl_buffer_size'];
|
|
if (!empty($options['time_limit'])) {
|
|
$this->_curlMulti->timeLimit = $options['time_limit'];
|
|
}
|
|
$this->_curlMulti->defaultCurlThreadOptions = array (
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_SSL_VERIFYHOST => false,
|
|
CURLOPT_RETURNTRANSFER => true, // return body
|
|
CURLOPT_HEADER => false, // return headers
|
|
CURLOPT_BINARYTRANSFER => true, // raw data
|
|
CURLOPT_FOLLOWLOCATION => true, // follow redirects
|
|
CURLOPT_ENCODING => "", // handle all encodings
|
|
CURLOPT_USERAGENT => "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", // who am i
|
|
CURLOPT_AUTOREFERER => true, // set referer on redirect
|
|
CURLOPT_CONNECTTIMEOUT => $options['curl_connect_timeout'], // timeout on connect
|
|
CURLOPT_TIMEOUT => $options['curl_timeout']
|
|
); // timeout on response
|
|
|
|
$data = array (
|
|
'url' => $url
|
|
);
|
|
$this->_getImageData($data);
|
|
|
|
$this->_curlMulti->process();
|
|
|
|
if (!empty($data['error'])) {
|
|
return array (
|
|
'error' => $data['error']
|
|
);
|
|
}
|
|
|
|
return $data['image_data'];
|
|
}
|
|
|
|
/**
|
|
* stop
|
|
*
|
|
* Stops fetch of images data
|
|
*/
|
|
public function stop() {
|
|
$this->_curlMulti->stop();
|
|
}
|
|
|
|
/**
|
|
* _getImageData
|
|
*
|
|
* First stop in processing every image.
|
|
*/
|
|
protected function _getImageData(&$data) {
|
|
$url = $data['url'];
|
|
|
|
if (empty($url)) {
|
|
$data['error'] = array (
|
|
'code' => 0,
|
|
'description' => 'URL not set'
|
|
);
|
|
$this->_triggerCallback($data);
|
|
return false;
|
|
}
|
|
|
|
$options = array (
|
|
CURLOPT_RANGE => '0-1'
|
|
);
|
|
|
|
$data['image_data'] = array (
|
|
'type' => null,
|
|
'width' => null,
|
|
'height' => null
|
|
);
|
|
|
|
$this->_curlMulti->transfer($url, $options, array (
|
|
$this,
|
|
'_imageTypeCallback'
|
|
), $data);
|
|
}
|
|
|
|
/**
|
|
* _imageTypeCallback
|
|
*
|
|
* Determinig image type.
|
|
*
|
|
* @internal Had to be public so CurlMulti can access it.
|
|
*/
|
|
public function _imageTypeCallback($url, $recived_data, $status_code, &$callback_data, $transfer_error_details) {
|
|
if (empty($recived_data)) {
|
|
$callback_data['error'] = array (
|
|
'code' => 1,
|
|
'description' => 'URL fetch failed',
|
|
'http_status' => $status_code
|
|
);
|
|
$this->_triggerCallback($callback_data);
|
|
return false;
|
|
}
|
|
|
|
$data = substr(bin2hex($recived_data), 0, 4);
|
|
|
|
switch ($data) {
|
|
case 'ffd8' : // jpeg
|
|
{
|
|
$callback_data['image_data']['type'] = 'jpeg';
|
|
|
|
// do auto buffer for jpeg images
|
|
if (0 === $this->_curlMulti->curlBufferSize) {
|
|
$options = array ();
|
|
$options[CURLOPT_HEADER] = true;
|
|
$options[CURLOPT_RANGE] = '0-1';
|
|
|
|
$this->_curlMulti->transfer($url, $options, array (
|
|
$this,
|
|
'_jpegReadSizeCallback'
|
|
), $callback_data);
|
|
} else {
|
|
$options = array ();
|
|
$options[CURLOPT_BUFFERSIZE] = $this->_curlMulti->curlBufferSize;
|
|
$options[CURLOPT_RETURNTRANSFER] = '';
|
|
$options[CURLOPT_WRITEFUNCTION] = array (
|
|
$this,
|
|
"_jpegTransferCallback"
|
|
);
|
|
|
|
$callback_data['streamed_buffer'] = '';
|
|
|
|
$this->_curlMulti->transfer($url, $options, array (
|
|
$this,
|
|
'_jpegReadCallback'
|
|
), $callback_data);
|
|
}
|
|
}
|
|
break;
|
|
case '4749' : // gif
|
|
{
|
|
$callback_data['image_data']['type'] = 'gif';
|
|
|
|
$options = array ();
|
|
$options[CURLOPT_RANGE] = '6-13';
|
|
|
|
$this->_curlMulti->transfer($url, $options, array (
|
|
$this,
|
|
'_gifReadCallback'
|
|
), $callback_data);
|
|
}
|
|
break;
|
|
case '8950' : // png
|
|
{
|
|
$callback_data['image_data']['type'] = 'png';
|
|
|
|
$options = array ();
|
|
$options[CURLOPT_RANGE] = '16-23';
|
|
|
|
$this->_curlMulti->transfer($url, $options, array (
|
|
$this,
|
|
'_pngReadCallback'
|
|
), $callback_data);
|
|
}
|
|
break;
|
|
default : // unknown type
|
|
{
|
|
$callback_data['error'] = array (
|
|
'code' => 2,
|
|
'description' => 'unknown image format',
|
|
'http_status' => $status_code
|
|
);
|
|
|
|
$this->_triggerCallback($callback_data);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* _jpegTransferCallback
|
|
*
|
|
* Curl data write buffer function.
|
|
* Image dimension are on different position within a file
|
|
* so this function will be called when buffer is ready a jpeg can be composed in memory,
|
|
* once dimension are known further transfer of jpeg file ends.
|
|
*
|
|
* @internal Had to be public so CurlMulti can access it.
|
|
*/
|
|
public function _jpegTransferCallback($ch, $data) {
|
|
$size_of_chunk = mb_strlen($data, '8bit');
|
|
|
|
$thread_data = $this->_curlMulti->getThreadDataByCurlHandler($ch);
|
|
$callback_data = & $thread_data['callback_data'];
|
|
|
|
if (!isset($callback_data['streamed_buffer']))
|
|
$callback_data['streamed_buffer'] = '';
|
|
$callback_data['streamed_buffer'] .= $data;
|
|
|
|
if (isset($callback_data['downloaded_size_trace'])) {
|
|
$callback_data['downloaded_size_trace'][] = array (
|
|
mb_strlen($callback_data['streamed_buffer'])
|
|
);
|
|
}
|
|
|
|
if (strlen($callback_data['streamed_buffer']) < 2) {
|
|
return $size_of_chunk;
|
|
}
|
|
|
|
// strip magic marker 0xFFD8
|
|
$operationalStreamedData = substr($callback_data['streamed_buffer'], 2);
|
|
|
|
do {
|
|
// can I read marker
|
|
if (strlen($operationalStreamedData) < 2) {
|
|
return $size_of_chunk;
|
|
}
|
|
|
|
$info = unpack('nmarker', $operationalStreamedData);
|
|
$operationalStreamedData = substr($operationalStreamedData, 2);
|
|
|
|
// only 0xFFC0 is of interest
|
|
if ($info['marker'] != 0xFFC0) {
|
|
// can I read length
|
|
if (strlen($operationalStreamedData) < 2) {
|
|
return $size_of_chunk;
|
|
}
|
|
|
|
// is block whole
|
|
$info = unpack('nlength', $operationalStreamedData);
|
|
if (strlen($operationalStreamedData) < $info['length']) {
|
|
return $size_of_chunk;
|
|
}
|
|
|
|
$operationalStreamedData = substr($operationalStreamedData, $info['length']);
|
|
continue;
|
|
}
|
|
|
|
// 0xFFC0 marker area
|
|
|
|
// can I read length
|
|
if (strlen($operationalStreamedData) < 2) {
|
|
return $size_of_chunk;
|
|
}
|
|
|
|
// is block whole
|
|
$info = unpack('nlength', $operationalStreamedData);
|
|
if (strlen($operationalStreamedData) < $info['length']) {
|
|
return $size_of_chunk;
|
|
}
|
|
$operationalStreamedData = substr($operationalStreamedData, 2);
|
|
|
|
// get data
|
|
$info = unpack('Cprecision/nY/nX', $operationalStreamedData);
|
|
|
|
$callback_data['image_data']['height'] = $info['Y'];
|
|
$callback_data['image_data']['width'] = $info['X'];
|
|
|
|
return 0; // stop reading data from source
|
|
} while (!empty($operationalStreamedData));
|
|
|
|
return $size_of_chunk;
|
|
}
|
|
|
|
/**
|
|
* _jpegReadCallback
|
|
*
|
|
* Will be called when jpeg data is fetched.
|
|
*
|
|
* @internal Had to be public so CurlMulti can access it.
|
|
*/
|
|
public function _jpegReadCallback($url, $recived_data, $status_code, &$callback_data, $transfer_error_details) {
|
|
if (isset($callback_data['streamed_buffer'])) {
|
|
unset($callback_data['streamed_buffer']);
|
|
}
|
|
|
|
if (empty($callback_data['image_data']['width'])) {
|
|
$callback_data['error'] = array (
|
|
'code' => 3,
|
|
'description' => 'jpeg image format read failed',
|
|
'http_status' => $status_code
|
|
);
|
|
$this->_triggerCallback($callback_data);
|
|
return;
|
|
}
|
|
|
|
$this->_triggerCallback($callback_data);
|
|
}
|
|
|
|
/**
|
|
* _jpegReadSizeCallback
|
|
*
|
|
* Will determine buffer size for fetching jpeg image based on it's size
|
|
*
|
|
* @internal Had to be public so CurlMulti can access it.
|
|
*/
|
|
public function _jpegReadSizeCallback($url, $recived_data, $status_code, &$callback_data, $transfer_error_details) {
|
|
$buffer_size = 1024;
|
|
|
|
if (false !== strpos($recived_data, 'Content-Range:')) {
|
|
$matches = array ();
|
|
preg_match('~Content-Range:\sbytes\s0-1/(\d+)\s~i', $recived_data, $matches);
|
|
if (isset($matches[1]) && !empty($matches[1])) {
|
|
$image_size = intval($matches[1]);
|
|
$buffer_size = intval($image_size * 0.1); // transfer in 10% chunks
|
|
|
|
if (isset($callback_data['buffer_size_trace'])) {
|
|
$callback_data['buffer_size_trace'][] = array (
|
|
$buffer_size
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
$options = array ();
|
|
$options[CURLOPT_BUFFERSIZE] = $buffer_size;
|
|
$options[CURLOPT_RETURNTRANSFER] = '';
|
|
$options[CURLOPT_WRITEFUNCTION] = array (
|
|
$this,
|
|
"_jpegTransferCallback"
|
|
);
|
|
|
|
$callback_data['streamed_buffer'] = '';
|
|
|
|
$this->_curlMulti->transfer($url, $options, array (
|
|
$this,
|
|
'_jpegReadCallback'
|
|
), $callback_data);
|
|
}
|
|
|
|
/**
|
|
* _gifReadCallback
|
|
*
|
|
* Will be called when gif data is fetched.
|
|
*
|
|
* @internal Had to be public so CurlMulti can access it.
|
|
*/
|
|
public function _gifReadCallback($url, $recived_data, $status_code, &$callback_data, $transfer_error_details) {
|
|
// server dose not Accept-Ranges
|
|
if (strlen($recived_data) > 8) {
|
|
$recived_data = substr($recived_data, 6, 8);
|
|
}
|
|
|
|
$imageWH = unpack('vwidth/vheight', $recived_data);
|
|
|
|
if (empty($imageWH['width'])) {
|
|
$callback_data['error'] = array (
|
|
'code' => 3,
|
|
'description' => 'gif image format read failed',
|
|
'http_status' => $status_code
|
|
);
|
|
$this->_triggerCallback($callback_data);
|
|
return;
|
|
}
|
|
|
|
$callback_data['image_data']['width'] = $imageWH['width'];
|
|
$callback_data['image_data']['height'] = $imageWH['height'];
|
|
|
|
$this->_triggerCallback($callback_data);
|
|
}
|
|
|
|
/**
|
|
* _pngReadCallback
|
|
*
|
|
* Will be called when png data is fetched.
|
|
*
|
|
* @internal Had to be public so CurlMulti can access it.
|
|
*/
|
|
public function _pngReadCallback($url, $recived_data, $status_code, &$callback_data, $transfer_error_details) {
|
|
// server dose not Accept-Ranges
|
|
if (strlen($recived_data) > 8) {
|
|
$recived_data = substr($recived_data, 16, 8);
|
|
}
|
|
|
|
$imageWH = unpack('Nwidth/Nheight', $recived_data);
|
|
|
|
if (empty($imageWH['width'])) {
|
|
$callback_data['error'] = array (
|
|
'code' => 3,
|
|
'description' => 'png image format read failed',
|
|
'http_status' => $status_code
|
|
);
|
|
$this->_triggerCallback($callback_data);
|
|
return;
|
|
}
|
|
|
|
$callback_data['image_data']['width'] = $imageWH['width'];
|
|
$callback_data['image_data']['height'] = $imageWH['height'];
|
|
|
|
$this->_triggerCallback($callback_data);
|
|
}
|
|
|
|
/**
|
|
* _triggerCallback
|
|
*
|
|
* Will trigger callback in clijent code for every single image.
|
|
*/
|
|
protected function _triggerCallback(&$data) {
|
|
if (!isset($data['callback']))
|
|
return;
|
|
|
|
$callback = $data['callback'];
|
|
unset($data['callback']);
|
|
|
|
if (!empty($callback)) {
|
|
$params = array ();
|
|
$params[] = $data;
|
|
$params[] = & $this;
|
|
|
|
call_user_func_array($callback, $params);
|
|
}
|
|
}
|
|
} |