PHPIndex

This page lists files in the current directory. You can view content, get download/execute commands for Wget, Curl, or PowerShell, or filter the list using wildcards (e.g., `*.sh`).

KodImageMagick.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/fileThumb/lib/KodImageMagick.class.php'
View Content
<?php 

/**
 * 图片处理类-ImageMagick命令行
 */
class KodImageMagick {
	private $plugin;
    private $command;
    public function __construct($plugin) {
        $this->plugin = $plugin;
    }

    // 格式是否支持
    public function isSupport($ext) {
        return true;
    }

    // 获取命令
    public function getCommand($type='convert') {
        return $this->plugin->getCommand($type);
    }

    /**
     * 图片生成缩略图
     * @param [type] $file
     * @param [type] $cacheFile
     * @param [type] $maxSize
     * @param [type] $ext
     * @return void
     */
	// imagemagick  -density 100 //耗时间,暂时去除
	// convert -density 900 banner.psd -colorspace RGB -resample 300 -trim -thumbnail 200x200 test.jpg
	// convert -colorspace rgb simple.pdf[0] -density 100 -sample 200x200 sample.jpg
    public function createThumb($file, $cacheFile, $maxSize, $ext) {
        if (!file_exists($file) || !$this->isSupport($ext)) return false;
        $command = $this->getCommand();
        if (!$command) return false;

		// 生成缩略图,不能比原来大;
		$sizeInfo = $this->getImgSize($file,$ext);
		if($sizeInfo && is_array($sizeInfo)){
			$sizeMin = min($sizeInfo[0],$sizeInfo[1]);
			$maxSize = $sizeMin ? min($sizeMin,$maxSize): 250;
		}
				
		$size  = $maxSize.'x'.$maxSize;
		$param = "-auto-orient -alpha off -quality 90 -size ".$size;
		$tempName = rand_string(15).'.png';
		switch ($ext){
			case 'eps':
			case 'psb':
			case 'psd':
			case 'ps'://ps,ai,pdf; ==> window:缺少组件;mac:命令行可以执行,但php执行有问题
			case 'ai':$file.= '[0]';break;
			case 'pdf':$file.= '[0]';
				$param = "-auto-orient -alpha remove -alpha off -quality 90 -size ".$size." -background white";
				break; // pdf 生成缩略图透明背景变为黑色问题处理;
			
			/**
			 * 生成doc/docx封面; or转图片;
			 * 
			 * mac: 使用liboffice=>soffice 关联convert的delegate;
			 * centos : unoconv (yum install unoconv); 
			 * 		转doc/docx/ppt/pptx/xls/xlsx/odt/odf为pdf; 再用convert提取某页为图片;
			 * 		实现office预览方案之一;(中文字体copy)
			 * 		unoconv -f pdf /data/from.docx /data/toxx.pdf;
			 * 		// https://github.com/ScoutsGidsenVL/Pydio/blob/master/plugins/editor.imagick/class.IMagickPreviewer.php
			 */
			case 'ppt':
			case 'pptx':
			case 'doc':
			case 'docx':$file.= '[0]';break;

			// https://legacy.imagemagick.org/Usage/thumbnails/
			case 'tif':$file.= '[0]';$param .= " -flatten ";break;
			//case 'gif':$file.= '[0]';break;
			case 'gif':
				$param = "-thumbnail ".$size;
				$tempName = rand_string(15).'.gif';
				break;
			
			case 'webp':
			case 'png':
			case 'bmp':$param = "-resize {$size}";break;
			case 'jpe':
			case 'jpg':
			case 'jpeg':
			case 'avif':
			case 'heic':$param = "-resize {$size} -auto-orient";break;
			
			default:
				$dng = 'dng,cr2,erf,raf,kdc,dcr,mrw,nrw,nef,orf,pef,x3f,srf,arw,sr2';
				$imageExt = $dng.',3fr,crw,dcm,fff,iiq,mdc,mef,mos,plt,ppm,raw,rw2,srw,tst';
				if(in_array($ext,explode(',',$imageExt))){
					$param = "-resize {$size}";
					//$file = 'rgb:'.$file.'[0]';
				}
				break;
		}
		// 移除元数据、最低压缩级别、禁止过滤,8位深度——可能执行快一点,内存占用没有区别
		if ($ext != 'gif') {
			$param .= ' -strip -define png:compression-level=1 -define png:filter=0 -depth 8';
		}
		$param = $this->plugin->memLimitParam('convert', $param);	// 加上内存限制

        $tempPath = $this->getTmpPath($cacheFile, $tempName);
		$this->setLctype($file,$tempPath);

		$script = $command.' '.$param.' '.escapeShell($file).' '.escapeShell($tempPath).' 2>&1';
		$script = "export MAGICK_THREAD_LIMIT=2; {$script}";	// 限制进程数
		$out = shell_exec($script);

		if(!file_exists($tempPath)) return $this->log('image thumb error:'.$out.';cmd='.$script);
		move_path($tempPath,$cacheFile);
		return true;
    }

    /**
     * 使用ffmpeg生成视频封面
     * @param [type] $file
     * @param [type] $cacheFile
     * @param [type] $videoThumbTime
     * @return void
     */
    public function createThumbVideo($file,$cacheFile,$videoThumbTime){
        $command = $this->getCommand('ffmpeg');
        if (!$command) return false;
        $tempPath = $this->getTmpPath($cacheFile, rand_string(15).'.jpg');

		$maxWidth = 800;
		$timeAt   = $videoThumbTime ? ' -ss 00:00:03' : '';	// 截取时间点,前置(ffmpeg -ss 00:00:03)可直接从第3秒开始处理,提升效率
		$this->setLctype($file,$tempPath);
		$script   = $this->plugin->memLimitParam('ffmpeg', $command) . $timeAt . ' -i '.escapeShell($file).' -y -f image2 -vframes 1 '.escapeShell($tempPath).' 2>&1';
		// $script = "/usr/bin/time -v ".$script;  // Maximum resident set size
		$out = shell_exec($script);
		if(!file_exists($tempPath)) {
			$this->log('video thumb error,'.$out.';cmd='.$script);
            return false;
		}

		move_path($tempPath,$cacheFile);
		$cm = new ImageThumb($cacheFile,'file');
		$cm->prorate($cacheFile,$maxWidth,$maxWidth);
        return true;
	}

    /**
     * 获取图片信息,类getimagesize格式
     */
    public function getImgSize($file, $ext = '') {
        if (!file_exists($file)) return false;

        // $res = shell_exec("identify -format '%wx%hx%[channels]x%[depth]' ".escapeshellarg($file));
        $res = shell_exec("identify -format '%w %h' -ping ".escapeshellarg($file));
        if (!$res) return false;

        list($width, $height) = explode(' ', trim($res));
        return array(
            intval($width),    // width
            intval($height),   // height
            'channels' => 3,
            'bits'     => 8,
        );
    }

    // 设置地区字符集,避免中文被过滤
	private function setLctype($path,$path2=''){
		if (stripos(setlocale(LC_CTYPE, 0), 'utf-8') !== false) return;
		if (Input::check($path,'hasChinese') || ($path2 && Input::check($path2,'hasChinese'))) {
			setlocale(LC_CTYPE, "en_US.UTF-8");
		}
	}

    // 重置临时文件位置:linux下$cacheFile不可写问题,先生成到/tmp下;再移动出来
    private function getTmpPath($tempFile, $tempName) {
        $tempPath = $tempFile;
        if($GLOBALS['config']['systemOS'] == 'linux' && is_writable('/tmp/')){
			mk_dir('/tmp/fileThumb');
			if(is_writable('/tmp/fileThumb')){ // 可能有不可写的情况;
				$tempPath = '/tmp/fileThumb/'.$tempName;
			}
		}
        return $tempPath;
    }

    // 记录日志
    public function log($msg) {
        $this->plugin->log($msg);
    }
    
}
KodImagick.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/fileThumb/lib/KodImagick.class.php'
View Content
<?php 

/**
 * 图片处理类-Imagick扩展
 */
class KodImagick {

    // 支持的图像格式
    private const IMAGE_FORMATS = array(
        'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tif', 'jpe', 'heic','avif'
    );
    // 支持的文档格式
    private const DOCUMENT_FORMATS = array(
        'psd', 'psb', 'eps', 'ai', 'pdf',
        // 'doc', 'docx', 'ppt', 'pptx',
    );
    // 支持的相机RAW格式
    private const RAW_FORMATS = array(
        'dng', 'cr2', 'erf', 'raf', 'kdc', 'dcr', 'mrw', 'nrw', 'nef', 'orf', 'pef',
        'x3f', 'srf', 'arw', 'sr2', '3fr', 'crw', 'dcm', 'fff', 'iiq', 'mdc', 'mef',
        'mos', 'plt', 'ppm', 'raw', 'rw2', 'srw', 'tst'
    );
    // 所有支持格式
    private $allFormats = array();

    private $plugin;
    private $defQuality = 85;
    private $defFormat = 'jpeg';
    private $maxResolution = 40000; // 支持的最大分辨率

    public function __construct($plugin) {
        $this->plugin = $plugin;
        $this->allFormats = array_merge(
            self::IMAGE_FORMATS, 
            self::DOCUMENT_FORMATS, 
            self::RAW_FORMATS
        );

        $this->setTmpDir();
        $this->setMemLimit();
    }

    // 设置Imagick临时目录
    private function setTmpDir() {
        if(!is_dir(TEMP_FILES)){mk_dir(TEMP_FILES);}
        $path = TEMP_FILES . '/imagick'; mk_dir($path);
        putenv('MAGICK_TEMPORARY_PATH='.$path);
        putenv('MAGICK_TMPDIR='.$path);
    }

    // 设置Imagick内存限制——实际是ImageMagick在占用系统内存,不受PHP内存限制
    private function setMemLimit() {
        $memFree = $this->getMemLimit();
        Imagick::setResourceLimit(Imagick::RESOURCETYPE_MEMORY, $memFree);
        Imagick::setResourceLimit(Imagick::RESOURCETYPE_MAP, $memFree * 2);
    }
    // 获取系统内存限制
    private function getMemLimit() {
        $memBase = 128 * 1024 * 1024; // 128M,最小内存限制——实际可用内存可能更小,暂不处理
		$memFree = $this->plugin->sysMemoryFree();
		return max($memBase, intval($memFree * 0.5));
    }

    // 格式是否支持
    public function isSupport($ext) {
        return in_array(strtolower($ext), $this->allFormats);
    }

    /**
     * 图片生成缩略图
     * @param [type] $file
     * @param [type] $cacheFile
     * @param [type] $maxSize
     * @param [type] $ext
     * @return void
     */
    public function createThumb($file, $cacheFile, $maxSize, $ext) {
        if (!file_exists($file) || !$this->isSupport($ext)) return false;

        $this->setMemLimit(); // 内存限制
        $imagick = null;

        try {
            $imagick = new Imagick();

            // 预读图像尺寸
            $imagick->pingImage($file);
            $orgWidth   = $imagick->getImageWidth();
            $orgHeight  = $imagick->getImageHeight();
            $imagick->clear();  // 清除ping结果
            // 限制超大图片
            if ($orgWidth > $this->maxResolution || $orgHeight > $this->maxResolution) {
                $msg = sprintf("Imagick convert error [%s]: Image too large: %s x %s", $file, $orgWidth, $orgHeight);
                $this->log($msg);
                return false;
            }
            // 启用像素缓存加速
            // $imagick->setOption('temporary-path', '/dev/shm');
            $imagick->setOption('cache:sync', '0');  // 禁用同步写入
            $imagick->setOption('sampling-factor', '4:2:0');  // 色度子采样
            $imagick->setOption('filter:blur', '0.8');        // 轻微模糊提升缩放速度

            // 读取图像
            if (in_array($ext, self::DOCUMENT_FORMATS)) {
                // 特殊格式处理
                $imagick->setResolution(300, 300);
                $imagick->readImage($file . '[0]');
            } else if (in_array($ext, self::RAW_FORMATS)) {
                // RAW格式处理
                $imagick->setResolution(300, 300);
                $imagick->readImage($file);
                $this->correctImageOrientation($imagick);
            } else if ($ext === 'gif' || $ext === 'tif') {
                // 多帧图像处理
                $imagick->readImage($file . '[0]');
            } else {
                // 超大图片,读取后立即缩小
                $maxReadSize = 5000; // 预读最大尺寸
                if ($orgWidth > $maxReadSize || $orgHeight > $maxReadSize) {
                    $ratio = min($maxReadSize / $orgWidth, $maxReadSize / $orgHeight);
                    $readWidth = intval($orgWidth * $ratio);
                    $readHeight = intval($orgHeight * $ratio);
                    // $imagick->sampleImage($readWidth, $readHeight);  // 先降采样
                    $imagick->readImage($file . '[' . $readWidth . 'x' . $readHeight . ']');
                    $imagick->scaleImage($readWidth, $readHeight, true); // 缩放
                } else {
                    // 普通图像
                    $imagick->readImage($file);
                }
            }

            // 获取实际图像(处理多页文档)
            $image = $imagick->getImage();

            // 自动旋转校正
            $this->autoOrientImage($image);
            // 颜色空间转换
            if ($image->getImageColorspace() == Imagick::COLORSPACE_CMYK) {
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
            }

            // 保持宽高比缩放
            $image->thumbnailImage($maxSize, 0);
            // $ratio = $maxSize / $orgWidth; // 缩略图的宽度比例
            // $newHeight = (int)($orgHeight * $ratio);
            // $image->resizeImage($maxSize, $newHeight, Imagick::FILTER_LANCZOS, 1);
            // $image->thumbnailImage($maxSize, 0, true, false); // 报错

            // 处理透明背景(当输出格式为JPEG时)
            if ($this->defFormat === 'jpeg' && $image->getImageAlphaChannel()) {
                $thumbWidth  = $image->getImageWidth();
                $thumbHeight = $image->getImageHeight();
                $background = new Imagick();
                $background->newImage(
                    $thumbWidth, 
                    $thumbHeight, 
                    'white',
                    'jpeg'
                );
                $background->compositeImage(
                    $image, 
                    Imagick::COMPOSITE_OVER, 
                    0, 0
                );
                $image = $background;
            }

            // 设置输出格式和质量
            $image->setImageFormat($this->defFormat);
            
            if ($this->defFormat === 'png') {
                // PNG压缩级别:0-9 (0=无压缩, 9=最大压缩)
                $compressionLevel = min(9, max(0, round(9 - ($this->defQuality / 10))));
                $image->setOption('png:compression-level', $compressionLevel);

                $image->setOption('png:exclude-chunk', 'all'); // 移除所有辅助块
                $image->setOption('png:compression-strategy', '1'); // 更快策略
            } else {
                $image->setImageCompressionQuality($this->defQuality);
            }

            // 移除元数据
            $image->stripImage();

            // 写入文件
            $result = $image->writeImage($cacheFile);
            return $result;
        } catch (Exception $e) {
            // Stack trace: $e->getTraceAsString()
            $msg = sprintf("Imagick convert error [%s]: %s", $file, $e->getMessage());
            $this->log($msg);
            return false;
        } finally {
            // 确保资源释放
            if ($imagick instanceof Imagick) {
                $imagick->clear();
                $imagick->destroy();
            }
        }
    }
    
    /**
     * 自动校正图像方向
     */
    private function autoOrientImage(Imagick $image) {
        $orientation = $image->getImageOrientation();
        
        switch ($orientation) {
            case Imagick::ORIENTATION_BOTTOMRIGHT:
                $image->rotateImage('#000', 180);
                break;
            case Imagick::ORIENTATION_RIGHTTOP:
                $image->rotateImage('#000', 90);
                break;
            case Imagick::ORIENTATION_LEFTBOTTOM:
                $image->rotateImage('#000', -90);
                break;
        }
        
        $image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
    }
    
    /**
     * 校正RAW格式图像方向
     */
    private function correctImageOrientation(Imagick $image) {
        try {
            // 尝试获取EXIF方向信息
            $exif = $image->getImageProperties('exif:*');
            if (isset($exif['exif:Orientation'])) {
                $orientation = (int)$exif['exif:Orientation'];
                $image->setImageOrientation($orientation);
                $this->autoOrientImage($image);
            }
        } catch (Exception $e) {
            // 忽略EXIF读取错误
        }
    }
    
    /**
     * 获取图片信息,类getimagesize格式
     */
    public function getImgSize($file, $ext = '') {
        if (!file_exists($file)) return false;

        $imagick = null;
        try {
            $imagick = new Imagick();
            // 特殊格式只读第一页/第一帧
            if (in_array($ext, self::DOCUMENT_FORMATS) || $ext === 'gif' || $ext === 'tif') {
                $imagick->pingImage($file . '[0]');
            } else {
                $imagick->pingImage($file); // readImage,使用pingImage避免加载像素
            }
            // $image = $imagick->getImage();
            return array (
                $imagick->getImageWidth(),
                $imagick->getImageHeight(),
                // 'channels'  => $image->getImageChannelCount(),
                'channels'  => 3,
                'bits'      => $imagick->getImageDepth(),
            );
        } catch (Exception $e) {
            return false;
        } finally {
            if ($imagick instanceof Imagick) {
                $imagick->clear();
                $imagick->destroy();
            }
        }
    }

    // 记录日志
    public function log($msg) {
        $this->plugin->log($msg);
    }
    
}
KodImaginary.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/fileThumb/lib/KodImaginary.class.php'
View Content
<?php 

/**
 * 图片处理类-Imaginary 服务
 * https://github.com/h2non/imaginary
 */
class KodImaginary {
    private $imgFormats = array(
        // 'bmp'  => 'image/(bmp|x-bitmap)',    // Unsupported media type
        'png'  => 'image/png',
        'jpg'  => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'gif'  => 'image/gif',   // 支持,但没必要
        'heic' => 'image/heic',
        'heif' => 'image/heif',
        'tif'  => 'image/tiff',
        'tiff' => 'image/tiff',
        'webp' => 'image/webp',
        // 'svg'   => 'image/svg+xml',
        'pdf'  => 'application/pdf',
        'ai'   => 'application/illustrator'
    );

    private $plugin;
    private $apiUrl;
    private $apiKey;
    private $urlKey;
    private $defQuality = 85;
    private $defFormat = 'jpeg';    // jpg不支持;png支持,但大小为jpeg的10+倍

    public function __construct($plugin){
        $this->plugin = $plugin;
        $this->initData();
    }
    // 初始化服务参数
    private function initData(){
        $config = $this->plugin->getConfig();
        $this->apiUrl = rtrim($config['imgnryHost'], '/');
        $this->apiKey = $config['imgnryApiKey']; // -key
        $this->urlKey = $config['imgnryUrlKey']; // -url-signature-key
    }

    // 检查服务状态
    public function status(){
        // $this->initData();  // 刷新配置参数
        $rest = $this->imgRequest('/health', array());
        return $rest ? true : false;
    }

    // 格式是否支持
    public function isSupport($ext){
        return isset($this->imgFormats[strtolower($ext)]);
    }

    /**
     * 图片生成缩略图
     * @param [type] $file
     * @param [type] $cacheFile
     * @param [type] $maxSize
     * @param [type] $ext
     * @return void
     */
    public function createThumb($file,$cacheFile,$maxSize,$ext) {
        if (request_url_safe($file)) {
            $url = $file;
        } else {
            if (!file_exists($file)) {return false;}
        }
        if (!$this->isSupport($ext)) return false;
        $this->image = $file;

        $data = array(
            'width'         => $maxSize,
            'height'        => 0, // 关键:高度设为 0 触发自适应
            'type'          => $this->defFormat, // 可选:输出格式(默认保持原格式)
            // 'quality'       => $this->defQuality,     // 可选:质量(默认自动)
            // 'smartcrop'     => 'true', // 智能裁剪(默认false),结合/thumbnail使用
            // 'nocrop'        => 'true', // 禁用裁剪(默认false)
            // 'norotation'    => 'true', // 禁用自动旋转(默认false)
            'stripmeta'     => 'true',  // 去除元数据
            'trace'         => 'true',
            'debug'         => 'true',
        );
        $post = array('file' => '@'.$file);
        if (isset($url)) {
            $post = array('url' => $this->parsePathUrl($url)); // 网络文件,需启用 -enable-url-source
        }
        $path = '/resize';      // 精确控制比例
        // $path = '/pipeline';    // 为图像执行系列操作,形成一个处理管道
        // $path = '/thumbnail';   // 方形/固定比例;/thumbnail+smartcrop=true
        $content = $this->imgRequest($path, $data, $post, $ext);
        if (!$content) return false;
        file_put_contents($cacheFile, $content);
        return true;
    }
    
    /**
     * 获取图片信息,类getimagesize格式
     * @param [type] $file
     * @return void
     */
    public function getImgSize($file, $ext='') {
        if (request_url_safe($file)) {
            $url = $file;
        } else if (file_exists($file)) {
            $url = Action('explorer.share')->link($file);   // localhost/127不支持访问,暂不处理
        }
        if (!$url) {return false;}
        if ($ext && !$this->isSupport($ext)) return false;
        $this->image = $file;

        // 需要是imaginary能访问的url
        $post = array('url' => $this->parsePathUrl($url));
        $info = $this->imgRequest('/info', array(), $post, $ext);
        if (!$info) return false;
        return array(
            $info['width'],
            $info['height'],
            'channels'	=> _get($info,'channels',4),
            'bits'		=> 8,
            // 'mime'		=> 'image/'.$info['type'],
        );
    }

    /**
     * 图片处理请求
     * @param [type] $path
     * @param [type] $data
     * @param boolean $post
     * @return void
     */
    private function imgRequest($path, $data, $post=array(), $ext='') {
        // key必须作为url参数传递,否则报错:Invalid or missing API key
        if (!empty($this->apiKey)) {
            $data['key'] = $this->apiKey;
        }
        $method = 'POST';
        if (isset($post['url'])) {
            $method = 'GET';
            $data['url'] = $post['url'];
            $post = array();
        }
        $query = http_build_query($data);
        $query = $this->signUrl($path, $query);
        $url = $this->apiUrl . $path . '?' . $query;
        $rest = url_request($url, $method, $post);
        // pr($url,$rest,$ext,$post);exit;
        if(!$rest || !isset($rest['data'])){
			$this->log("imaginary {$path} error: [{$this->image}] request failed");
            return false;
		}
        $data = json_decode($rest['data'],true);
        if (!$rest['status'] || $rest['code'] != 200) {
            $msg = _get($data, 'message', 'unknown error');
            $this->log("imaginary {$path} error: [{$this->image}] " . $msg);
            return false;
        }
        return $data ? $data : $rest['data'];
    }

    /**
     * 为Imaginary URL生成签名
     */
    private function signUrl($path, $query) {
        if (empty($this->urlKey)) {return $query;}

        $toSign = $path . '?' . $query;
        $signature = base64_encode(hash_hmac('sha1', $toSign, $this->urlKey, true));
        $signature = rtrim(strtr($signature, '+/', '-_'), '=');
        return $query . '&signature=' . urlencode($signature);
    }

    // 记录日志
    public function log($msg) {
        $this->plugin->log($msg);
    }

    // 非同一服务器localhost访问自动适配;后续可增加外网访问适配
    public function parsePathUrl($url) {
        $parse1 = parse_url($url);
        $parse2 = parse_url($this->apiUrl);
        if ($parse1['host'] != $parse2['host']) {
            if ($parse1['host'] == 'localhost') {
                $server = new ServerInfo();
                $ip = method_exists($server, 'getInternalIP') ? $server->getInternalIP() : false;   // get_server_ip();
                if ($ip) {
                    $port = $parse1['port'] && $parse1['port'] != 80 ? ':' . $parse1['port'] : '';
                    $url  = $parse1['scheme'] . '://' . $ip . $port . $parse1['path'] . (isset($parse1['query']) ? '?' . $parse1['query'] : '');
                }
            }
        }
        return $url;
    }

}
TaskConvert.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/fileThumb/lib/TaskConvert.class.php'
View Content
<?php
/*
* @link http://kodcloud.com/
* @author warlee | e-mail:kodcloud@qq.com
* @copyright warlee 2014.(Shanghai)Co.,Ltd
* @license http://kodcloud.com/tools/license/license.txt
*/
/**
 * 转码任务处理;
 */
class TaskConvert extends Task{
	public function onKillSet($call,$args=array()){
		$this->onKillCall = array($call,$args);
	}
	public function onKill(){
		if(!$this->onKillCall) return;
		ActionApply($this->onKillCall[0],$this->onKillCall[1]);
		$this->onKillCall = false;
	}
	protected function endAfter(){
		$this->onKill();
	}
}
VideoResize.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/fileThumb/lib/VideoResize.class.php'
View Content
<?php

/**
 * 视频转码处理
 * 
 * 转码条件: 视频文件格式,已解析到播放时长,大于20M,非系统文件
 * 任务管理: 转码后进入后台执行,旁路定时获取转码进度,更新到任务; 任务可手动结束,结束后同时结束转码进程;
 * 转码配置: 开启转码,最大并发进程,最小文件大小;(任务列表)
 * 并发管理: 进行中则返回进度; 添加时检测允许中的任务,大于最大限制则直接返回信息; 
 * 播放处理: 开启了转码时,播放时请求该文件标清视频(已完成返回标清视频链接;首次触发转码;进行中则获取进度;任务繁忙情况);
 * 
 * test: http://127.0.0.1/kod/kodbox/?plugin/fileThumb/videoSmall&path={source:3801668}
 * 视频压制ffmpeg参数: https://www.bilibili.com/read/cv5804639/
 * ffmpeg参数说明: https://github.com/fujiawei-dev/ffmpeg-generator/blob/master/docs/vfilters.md
 * 部分服务器处理视频转码绿色条纹花屏问题(兼容处理)
 * 		图片缩放指定转码算法 -sws_flags, 默认为bilinear, ok=accurate_rnd,neighbor,bitexact(neighbor会有锯齿)
 * 		https://www.sohu.com/a/551562728_121066035 
 * 		https://www.cnblogs.com/acloud/archive/2011/10/29/sws_scale.html
 * 		https://github.com/hemanthm/ffmpeg-soc/blob/376d5c5f13/libswscale/options.c
 * 		https://blog.csdn.net/Randy009/article/details/51523331
 */
class videoResize {
	const STATUS_SUCCESS	= 2;	//成功;
	const STATUS_IGNORE		= 3;	//忽略转换,大小格式等不符合;
	const STATUS_ERROR		= 4;	//转换失败,ffmepg不存在等原因;
	const STATUS_RUNNING	= 5;	//转换进行中;
	const STATUS_LIMIT		= 6;	//任务数达到上限,忽略该任务.

	public function start($plugin){
		$path = $plugin->filePath($GLOBALS['in']['path']);
		$fileInfo = IO::info($path);
		$fileInfo = Action('explorer.list')->pathInfoMore($fileInfo);
		$fileInfo['taskID'] = 'video-convert-'.KodIO::hashPath($fileInfo);
		$status  = $this->run($path,$fileInfo,$plugin);
		if($status === true) return;
		
		$taskID = $fileInfo['taskID'];
		$result = array('status'=>$status,'taskRun'=>Cache::get('fileThumb-videoResizeCount'));
		switch ($status) {
			case self::STATUS_SUCCESS:
				$findSource = IO::fileNameExist($plugin->cachePath,$taskID.".mp4");
				$sourcePath = KodIO::make($findSource);
				$result['msg']  = LNG('fileThumb.video.STATUS_SUCCESS');
				$result['data'] = $plugin->pluginApi.'videoSmall&play=1&path='.rawurlencode($path);
				$result['data'] = Action('explorer.share')->link($sourcePath);
				$result['size'] = array(
					'before'	=> size_format($fileInfo['size']),
					'now'		=> size_format(IO::size($sourcePath)),
				);
				// if($_GET['play'] == '1'){IO::fileOut($sourcePath);exit;}
				break;
			case self::STATUS_IGNORE:$result['msg'] = LNG('fileThumb.video.STATUS_IGNORE');break;
			case self::STATUS_ERROR:$result['msg'] = $this->convertError($taskID);break;
			case self::STATUS_RUNNING:
				$result['msg']  = LNG('fileThumb.video.STATUS_RUNNING');
				$result['data'] = Task::get($taskID);
				$errorCheck = 'Task_error_'.$taskID;
				if(!$result['data']){
				    $lastError = Cache::get($errorCheck);
				    if(!$lastError){Cache::set($errorCheck,time(),60);}
				    if($lastError && time() - $lastError > 10){
				        $this->convertClear($taskID);
				        Cache::remove($errorCheck);
				    }
				}else{
				    Cache::remove($errorCheck);
				}
				break;
			case self::STATUS_LIMIT:$result['msg'] = LNG('fileThumb.video.STATUS_LIMIT');break;
			default:break;
		}
		show_json($result);
	}
	public function run($path,$fileInfo,$plugin){
		// io缩略图已存在,直接输出
		$cachePath = $plugin->cachePath;
		$taskID    = $fileInfo['taskID'];
		$tempFileName = $taskID.".mp4";
		$config 	= $plugin->getConfig('fileThumb');
		$isVideo   	= in_array($fileInfo['ext'],explode(',',$config['videoConvertType']));
		$fileSizeMax = floatval($config['videoConvertLimitTo']); //GB; 为0则不限制
		$fileSizeMin = floatval($config['videoConvertLimit']); //GB; 为0则不限制
		if(IO::fileNameExist($cachePath, $tempFileName)){return self::STATUS_SUCCESS;}
		
		// 部分文件无法获取视频信息(或时长),导致无法执行转码
		if ($isVideo) {
			if (!isset($fileInfo['fileInfoMore'])) $fileInfo['fileInfoMore'] = array();
			if (!isset($fileInfo['fileInfoMore']['playtime'])) $fileInfo['fileInfoMore']['playtime'] = 0;
		}
		$pathDisplay = isset($fileInfo['pathDisplay']) ? $fileInfo['pathDisplay'] : $fileInfo['path'];
		if( !$isVideo || 
		    !is_array($fileInfo['fileInfoMore']) ||
			!isset($fileInfo['fileInfoMore']['playtime']) ||
			strstr($pathDisplay,'/systemPath/systemTemp/plugin/fileThumb/') ||
			strstr($pathDisplay,'/tmp/fileThumb') ||
			strstr($pathDisplay,TEMP_FILES) ||
			($fileSizeMax > 0.01 && $fileInfo['size'] > 1024 * 1024 * 1024 * $fileSizeMax) ||
			($fileSizeMin > 0.01 && $fileInfo['size'] < 1024 * 1024 * $fileSizeMin) ){
			return self::STATUS_IGNORE;
		}
		if($this->convertError($taskID)){return self::STATUS_ERROR;} // 上次转换失败缓存记录;
		
		$command = $plugin->getFFmpeg();
		if(!$command || !function_exists('proc_open') || !function_exists('shell_exec')){
			$this->convertError($taskID,LNG('fileThumb.video.STATUS_ERROR').'(3501)',600);
			return self::STATUS_ERROR;//Ffmpeg 软件未找到,请安装后再试
		}
		if(!$this->convertSupport($command)){
			$this->convertError($taskID,'ffmpeg not support libx264; please repeat install'.'(3502)',600);
			return self::STATUS_ERROR;//Ffmpeg 转码解码器不支持;
		}
		
		// 过短的视频封面图,不指定时间;
		$localFile = $plugin->localFile($path);
		if(Cache::get($taskID) == 'error'){return self::STATUS_ERROR;}; //是否已经转码
		if(!$localFile ){
			Cache::set($taskID,'error',60);
			$this->convertError($taskID,'localFile move error!'.'(3503)',60);
			return self::STATUS_IGNORE;
		}
		
		$tempPath = TEMP_FILES.$tempFileName;
		if($GLOBALS['config']['systemOS'] == 'linux' && is_writable('/tmp/')){
			$tempPath = '/tmp/fileThumb/'.$tempFileName;
		}
		mk_dir(get_path_father($tempPath));

		if(Cache::get($taskID) == 'running') return self::STATUS_RUNNING;
		$runTaskMax   = intval($config['videoConvertTask']);
		$runTaskCount = intval(Cache::get('fileThumb-videoResizeCount'));
		if($runTaskCount >= $runTaskMax) return self::STATUS_LIMIT;
		if($_GET['noOutput'] == '1'){http_close();} // 默认输出;
		$this->convertAdd($taskID);
		
		// https://www.ruanyifeng.com/blog/2020/01/ffmpeg.html
		ignore_timeout();
		$quality 	= 'scale=-2:480 -b:v 1000k -maxrate 1000k -sws_flags accurate_rnd';//480:-2 -2:480 限制码率; 缩放图片处理
		$timeStart 	= time();// 画质/速度: medium/ultrafast/fast
		$logFile 	= $tempPath.'.log';@unlink($logFile);//-vf scale=480:-2 -b:v 1024k -maxrate 1000k -threads 2
		$args 		= '-c:a aac -preset medium -vf '.$quality.' -strict -2 -c:v libx264 1>'.$logFile.' 2>&1';
		$script 	= $plugin->memLimitParam('ffmpeg', $command).' -y -i "'.$localFile.'" '.$args.' "'.$tempPath.'"';
		
		// 后台运行
		if($GLOBALS['config']['systemOS'] == 'windows'){
			$script = 'start /B "" '.$script;
		}else{
			$script = $script.' &';
		}
		$this->log('[command] '.$script."\n");
		//@shell_exec($script);@shell_exec("powershell.exe Start-Process -WindowStyle hidden '".$command."' ");
        proc_close(proc_open($script,array(array('pipe','r'),array('pipe','w'),array('pipe','w')),$pipes));
		$this->progress($tempPath,$fileInfo,$cachePath,$timeStart);
		return true;
	}
	
	/**
	 * 启动转换后计算进度, 并存储在全局任务中;
	 * 定时检测转换进度: 300ms一次, 结束条件: 转换完成or输出日志文件5s没有更新;
	 */
	private function progress($tempPath,$fileInfo,$cachePath,$timeStart){
		$logFile = $tempPath.'.log';// 5s没有更新则结束;
		usleep(300*1000);// 运行后,等待一段时间再读取信息;
		
		$tempName = get_path_this($tempPath);
		$pid  = $this->processFind($tempName);
		$data = $this->progressGet($logFile);
		$args = array($tempPath,$fileInfo,$cachePath,$timeStart,$pid);
		$task = new TaskConvert($fileInfo['taskID'],'videoResize',$data['total'],LNG("fileThumb.video.title"));
		$task->task['currentTitle'] = size_format($fileInfo['size']).'; '.$fileInfo['name'];
		$task->onKillCall = array(array($this,'convertFinished'),$args);
		$task->onKillSet(array($this,'convertFinished'),$args);
		
		$this->log('[start] '.$task->task['currentTitle'].'; pid='.$pid);
		while(true){
			$data = $this->progressGet($logFile);clearstatcache();
			
			//进程已不存在; 转码报错或者意外终止或者其他情况的进程终止;
			if($data['total'] && $data['finished'] == $data['total']){$this->log('stop-finished;');break;}
			if(time() - @filemtime($logFile) >= 20){$this->log('stop-time;');break;}
			if(!$this->processFind($tempName)){$this->log('stop-pid;');break;} 

			$task->task['taskFinished'] = round($data['finished'],3);
			CacheLock::lock("video-process-update");
			$task->update(0);
			CacheLock::unlock("video-process-update");
			$this->log(sprintf('%.1f',$data['finished'] * 100 / $data['total']).'%',true);
			Cache::set($fileInfo['taskID'],'running',60);
			usleep(300*1000);//300ms;
		}
		$task->end();$task->onKill();
	}
	public function log($log,$clear=false,$show=true){
	    if($show){echoLog($log,$clear);}
		if(!$clear){write_log($log,'videoConvert');}
	}
	
	private function progressGet($logFile){
		$result  = array('finished'=>0,'total'=>0);
		$content = @file_get_contents($logFile);
		preg_match("/Duration: (.*?), start: (.*?), bitrate:/", $content, $match);
		if(is_array($match)){
			$total = explode(':', $match[1]);
			$result['total'] = intval($total[0]) * 3600 + intval($total[1]) * 60 + floatval($total[2]); // 转换为秒
		}

		preg_match_all("/frame=(.*?) time=(.*?) bitrate/", $content, $match);
		if(is_array($match) && count($match) == 3 && $match[2]){
			$total = explode(':',$match[2][count($match[2]) - 1]);
			$result['finished'] = intval($total[0]) * 3600 + intval($total[1]) * 60 + floatval($total[2]); // 转换为秒
		}
		
		if(!$result['total']){$result['total'] = 1;}
		if(strstr($content,'video:')){$result['finished'] = $result['total'];}
		return $result;
	}	
	
	public function convertFinished($tempPath,$fileInfo,$cachePath,$timeStart,$pid){
		$logFile 	= $tempPath.'.log';
		$data 		= $this->progressGet($logFile);
		$output 	= @file_get_contents($logFile);
		@unlink($logFile);
		$this->processKill($pid);
		$this->convertClear($fileInfo['taskID']);
		
		$runError  = true;
		$errorTips = 'Run error!';$cacheTime = 3600;
		if( preg_match("/(Error .*)/",$output,$match) || 
			preg_match("/(Unknown encoder .*)/",$output,$match) ||
			preg_match("/(Invalid data found .*)/",$output,$match) ||
			preg_match("/(No device available .*)/",$output,$match)
		){
			$errorTips = '[ffmpeg error] '.$match[0].';<br/>see log[data/temp/log/videoconvert/xx.log]';
		}
		if( preg_match("/frame=\s+(\d+)/",$output,$match)){
			$errorTips = 'Stoped!';
			$runError  = false;
		}
		$logEnd  = get_caller_msg();
		$logTime = 'time='.(time() - $timeStart);	
		if( $data['total'] && intval($data['total']) == intval($data['finished']) && 
			file_exists($tempPath) && is_file($tempPath) && filesize($tempPath) > 100 ){
			$runError = true;
			$destPath = IO::move($tempPath,$cachePath);
			$checkStr = IO::fileSubstr($destPath,0,10);
			if($checkStr && strlen($checkStr) == 10){
				$this->log('[end] '.$fileInfo['name'].'; finished Success; '.$logTime.$logEnd);
				return;
			}
			IO::remove($destPath,false);
			$this->log('[end] '.$fileInfo['name'].'; move error; '.$logTime.$logEnd);
			$errorTips = 'Move temp file error!';$cacheTime = 60;
		}
		
		@unlink($tempPath);
		Cache::set($fileInfo['taskID'],'error',5);
		$this->convertError($fileInfo['taskID'],$errorTips,$cacheTime);
		$logAdd = $runError ? "\n".trim($output) : '';
		$this->log('[end] '.$fileInfo['name'].';'.$errorTips.'; '.$logTime.$logAdd.$logEnd);
		$this->log('[end] '.$output);
	}
	
	public function convertAdd($taskID){
		$runTaskCount = intval(Cache::get('fileThumb-videoResizeCount'));
		$taskList     = Cache::get('fileThumb-videoResizeList');
		$taskList	  = is_array($taskList) ? $taskList : array();
		if(!$runTaskCount){$this->stopAll();$taskList = array();}
		
		$taskList[$taskID] = '1';
		Cache::set('fileThumb-videoResizeList',$taskList,3600);
		Cache::set('fileThumb-videoResizeCount',($runTaskCount + 1),3600);
		Cache::set($taskID,'running',600);
	}
	public function convertClear($taskID){
		$runTaskCount = intval(Cache::get('fileThumb-videoResizeCount'));
		$runTaskCount = $runTaskCount <= 1 ? 0 : ($runTaskCount - 1);

		$taskList     = Cache::get('fileThumb-videoResizeList');
		$taskList	  = is_array($taskList) ? $taskList : array();
		if($taskList[$taskID]){unset($taskList[$taskID]);}
		
		Cache::set('fileThumb-videoResizeList',$taskList,3600);		
		Cache::set('fileThumb-videoResizeCount',$runTaskCount,3600);
		Cache::remove($taskID);
		if(!$runTaskCount){$this->stopAll();}
	}
	public function stopAll(){
		$taskList = Cache::get('fileThumb-videoResizeList');
		$taskList = is_array($taskList) ? $taskList : array();
		foreach ($taskList as $taskID=>$key){
			Task::kill($taskID);
			Cache::remove($taskID);
			$this->convertError($taskID,-1);
		}
		Cache::remove('fileThumb-videoResizeList');
		Cache::remove('fileThumb-videoResizeCount');
		
		$count = 0;
		while($count <= 20){
			$pid = $this->processFind('ffmpeg');
			if($pid){$this->processKill($pid);}
			if(!$pid){break;}
			$count ++;
		}
	}
	private function convertError($taskID,$content="",$cacheTime=0){
		$key = 'fileThumb-videoResizeError-'.$taskID;
		if($content === -1){return Cache::remove($key);}
		if(!$content){return Cache::get($key);}
		
		if(!$cacheTime){$cacheTime = 3600*24*3;}
		Cache::set($key,$content,$cacheTime);
	}
	private function convertSupport($ffmpeg){
		$out = shell_exec($ffmpeg.' -v 2>&1');
		if(!strstr($out,'--enable-libx264')){return false;}
		return true;
	}
	
	// 通过命令行及参数查找到进程pid; 兼容Linux,window,mac
	// http://blog.kail.xyz/post/2018-03-28/other/windows-find-kill.html
	public function processFind($search){
		$this->setLctype($search);
		$cmd = "ps -eo user,pid,ppid,args | grep '".escapeShell($search)."' | grep -v grep | awk '{print $2}'";
		if($GLOBALS['config']['systemOS'] != 'windows'){return trim(@shell_exec($cmd));}
		
		// windows 获取pid;
		$cmd = 'WMIC process where "Commandline like \'%%%'.$search.'%%%\'" get Caption,Processid,Commandline';
		$res = trim(@shell_exec($cmd));
		$resArr = explode("\n",trim($res));
		if(!$resArr || count($resArr) <= 3) return '';

		$lineFind = $resArr[count($resArr) - 3];// 最后两个一个为wmic,和cmd;
		$res = preg_match("/.*\s+(\d+)\s*$/",$lineFind,$match);
		if($res && is_array($match) && $match[1]){return $match[1];}
		return '';
	}
	
	// 通过pid结束进程;
	public function processKill($pid){
		if(!$pid) return;
		if($GLOBALS['config']['systemOS'] != 'windows'){return @shell_exec('kill -9 '.$pid);}
		@shell_exec('taskkill /F /PID '.$pid);
	}
	
	

	// ================================== 生成视频预览图 ==================================
	// 生成视频预览图; 平均截取300张图(小于30s的视频不截取)
	public function videoPreview($plugin){
		$pickCount = 300;// 总共截取图片数(平均时间内截取)
		$path = $plugin->filePath($GLOBALS['in']['path']);
		$fileInfo = IO::info($path);
		$fileInfo = Action('explorer.list')->pathInfoMore($fileInfo);
		$tempFileName = 'preview-'.KodIO::hashPath($fileInfo).'.jpg';
		$findSource = IO::fileNameExist($plugin->cachePath,$tempFileName);
		if($findSource){return IO::fileOut(KodIO::make($findSource));}

		$command = $plugin->getFFmpeg();
		if(!$command){return show_json('command error',false);}
		$localFile = $plugin->localFile($path);
		if(!$localFile ){return show_json('not local file',false);}
		$videoInfo = $this->parseVideoInfo($command,$localFile);
		$this->storeVideoMetadata($fileInfo,$videoInfo);
		if(!$videoInfo || $videoInfo['playtime'] <= 30){return show_json('time too short!',false);}
		
		// $pickCount = $totalTime; 每秒生成一张图片; 可能很大
		// 宽列数固定10幅图, 行数为总截取图除一行列数; https://blog.51cto.com/u_15639793/5297432
		if(Cache::get($tempFileName)) return show_json('running');
		Cache::set($tempFileName,'running',600);
		
		ignore_timeout();
		$tempPath = TEMP_FILES.$tempFileName;
		$fps = $pickCount / $videoInfo['playtime'];
		$sizeW = 150;
		$tile  = '10x'.ceil($pickCount / 10).''; 
		$scale = 'scale='.$sizeW.':-2'; //,pad='.$sizeW.':'.$sizeH.':-1:-1 -q:v 1~5 ;质量从最好到最差;
		$args  = '-sws_flags accurate_rnd -q:v 4 -an'; //更多参数; 设置图片缩放算法(不设置缩小时可能产生绿色条纹花屏);
		$this->setLctype($localFile,$tempPath);
		$cmd = $plugin->memLimitParam('ffmpeg', $command).' -y -i '.escapeShell($localFile).' -vf "fps='.$fps.','.$scale.',tile='.$tile.'" '.$args.' '.escapeShell($tempPath);
		$this->log('[videoPreview start] '.$fileInfo['name'].';size='.size_format($fileInfo['size']),0,0);$timeStart = timeFloat();
		$this->log('[videoPreview run] '.$cmd,0,0);
		@shell_exec($cmd);//pr($cmd);exit;
		Cache::remove($tempFileName);
		
		$success = file_exists($tempPath) && filesize($tempPath) > 100;
		$msg     = $success ? 'success' : 'error';
		$this->log('[videoPreview end] '.$fileInfo['name'].';time='.(timeFloat() - $timeStart).'s;'.$msg,0,0);
		if($success){
			$destPath = IO::move($tempPath,$plugin->cachePath);
			if($destPath){return IO::fileOut($destPath);}
		}
		@unlink($tempPath);
		show_json('run error',false);
	}
	
	// 更新完善文件Meta信息;
	private function storeVideoMetadata($fileInfo,$videoInfo){
		$infoMore = _get($fileInfo,'fileInfoMore',array());
		$cacheKey = 'fileInfo.'.md5($fileInfo['path'].'@'.$fileInfo['size'].$fileInfo['modifyTime']);
		$fileID   = _get($fileInfo,'fileID');
		$fileID   = _get($fileInfo,'fileInfo.fileID',$fileID);

		$audio    = $videoInfo['audio'];
		$infoMore['audio'] = is_array($infoMore['audio']) ? array_merge($audio,$infoMore['audio']) : $audio;
		$infoMore = array_merge($videoInfo,$infoMore);
		$infoMore['etag']  = $fileID ? $fileID : $cacheKey;
		if($fileID){
			Model("File")->metaSet($fileID,'fileInfoMore',json_encode($infoMore));
		}else{
			Cache::set($cacheKey,$infoMore,3600*24*30);
		}
	}

	// 解析视频文件信息;
	private function parseVideoInfo($command,$video){
		$this->setLctype($video);
		$result  = shell_exec($command.' -i '.escapeShell($video).' 2>&1');
		$info    = array('playtime'=>0,'createTime'=>'','audio'=>array());
		if(preg_match("/Duration:\s*([0-9\.\:]+),/", $result, $match)) {
			$total = explode(':', $match[1]);
			$info['playtime'] = intval($total[0]) * 3600 + intval($total[1]) * 60 + floatval($total[2]); // 转换为秒
		}
		if(preg_match("/Video: (.*?), (.*?), (\d+)x(\d+)/", $result,$match)) {
			$info['dataformat'] = $match[1];
			$info['sizeWidth']  = $match[3];
			$info['sizeHeight'] = $match[4];
		}
		if(preg_match("/Audio: (.*?), (\d+) Hz, (.*?), /", $result,$match)) {
			$info['audio']['dataformat'] = $match[1];
			$info['audio']['rate']  = $match[2];
			$info['audio']['channelmode']  = $match[3];
		}
		if(preg_match("/creation_time\s*:\s*(.*)/", $result,$match)){
			$info['createTime'] = date("Y-m-d H:i:s",strtotime($match[1]));
		}
		if(preg_match("/encoder\s*:\s*(.*)/", $result,$match)){
			$info['software'] = $match[1];
		}
		return $info;
	}
	
	
	private function findBinPath($bin,$check){
		$isWin = $GLOBALS['config']['systemOS'] == 'windows';
		$path  = false;
		if ($isWin) {
			exec('where '.escapeshellarg($bin),$output);
			foreach ($output as $line) {
				if (stripos($line,$check) !== false) {
					$path = escapeshellarg($line).'/'.$bin.'.exe';
					break;
				}
			}
		} else {
			exec('which '.$bin,$output,$status);
			if ($status == 0) {
				$path = $bin;
				if($output[0]){
					$path = $output[0].'/'.$bin;
				}
			}
		}
		return $path;
	}

	// 设置地区字符集,避免中文被过滤
	private function setLctype($path,$path2=''){
		if (stripos(setlocale(LC_CTYPE, 0), 'utf-8') !== false) return;
		if (Input::check($path,'hasChinese') || ($path2 && Input::check($path2,'hasChinese'))) {
			setlocale(LC_CTYPE, "en_US.UTF-8");
		}
	}
}