hookRegist(array( 'user.commonJs.insert' => 'fileThumbPlugin.echoJs', 'explorer.list.path.parse' => 'fileThumbPlugin.listParse', 'explorer.list.itemParse' => 'fileThumbPlugin.itemParse', )); } public function echoJs(){ $this->echoFile('static/main.js'); } public function onUpdate(){ AutoTask::restart();sleep(3); AutoTask::start(); } /** * 优化版,列表统一集中处理; 拉取列表时加入队列处理(同时加入预览大图转换); 1 * 总大概性能: 1000图片/s; checkCoverExists==>1000条约300ms; 缩略图处理==>1000条约200ms;Cache::get(); 10000次/s; */ public function listParse($data){ $current = is_array($data['current']) ? $data['current']:array(); if(!$data || !is_array($data['fileList']) || !count($data['fileList'])){return $data;} if(!$this->getConvert() || !$this->getFFmpeg()){return $data;} if($current && $current['targetType'] == 'system' && strstr($current['pathDisplay'],'plugin/fileThumb/')){return $data;} $config = $this->getConfig('fileThumb'); $supportWeb = explode(',','png,jpg,jpeg,bmp,webp,gif'); $supportThumb = explode(',',$config['fileThumb'].','.implode(',',$supportWeb)); $supportView = explode(',',$config['fileExt'].','.implode(',',$supportWeb)); $cachePath = false;$timeStart = timeFloat(); // 遍历文件列表,筛选出需要加入缩略图及显示图的内容(支持预览的,加入fileShowView) $coverList = array(); foreach($data['fileList'] as &$file){ if(!$file['ext'] || $file['size'] <= 100){continue;} if(!in_array($file['ext'],$supportThumb)){continue;} if(!kodIO::allowCover($file)){continue;} if(timeFloat() - $timeStart >= 10){break;} // 内容太多,超出时间则不再处理; if(in_array($file['ext'],$supportWeb) && $file['size'] <= 1024*50){continue;} if(!$cachePath){$cachePath = $this->pluginCachePath();} $fileHash = KodIO::hashPath($file);$path = $file['path']; $coverList[$path] = array('fileThumb'=>array('cover'=>"cover_".$fileHash."_250.png",'width'=>250)); if(in_array($file['ext'],$supportView)){ $coverList[$path]['fileShowView'] = array('cover'=>"cover_".$fileHash."_1200.png",'width'=>1200); } };unset($file); if(!$cachePath || !$coverList){return $data;};//return $data; // 处理需要加入缩略图的文件; 查询数据库已存在的缓存图片;查询redis缓存(已处理,队列中,出错了:不处理)-->未在队列--加入队列; $needMake = 0;$makeIndex = 0; // 待生成缩略图数量,不包含大图; $coverList = $this->checkCoverExists($cachePath,$coverList,$needMake); $makeCoverNow = $needMake <= 3 ? true:false; // 待生成列表小于5,则缩略图调用立即生成; $obj = Action('user.index'); foreach($data['fileList'] as &$file){ if(!isset($coverList[$file['path']])){continue;} if(timeFloat() - $timeStart >= 10){break;} // 内容太多,超出时间则不再处理; $path = $file['path']; foreach($coverList[$path] as $thumbKey=>$item){ $coverName = $item['cover'];// 少量没有缩略图的内容,立即生成处理; $param = array('path'=>$path,'etag'=>$file['modifyTime'],'width'=>$item['width']); $imageSrc = $obj->apiSignMake('plugin/fileThumb/cover',$param); if(isset($item['sourceID']) && $item['sourceID']){$file[$thumbKey] = $imageSrc;continue;} if($thumbKey == 'fileShowView'){$file[$thumbKey] = $imageSrc;}// 预览大图,不检测是否有缓存 if($thumbKey == 'fileThumb' && $makeCoverNow){$file[$thumbKey] = $imageSrc;continue;} // 多张图片待生成缩略图时,第一张处理为触发调用后台任务队列; if($thumbKey == 'fileThumb'){$makeIndex++;} if($thumbKey == 'fileThumb' && $makeIndex <= 2){ $file[$thumbKey] = APP_HOST.'index.php?user/view/call&_t='.rand_string(5); } $cacheType = Cache::get($coverName); if($cacheType == 'no' || $cacheType == 'queue'){continue;} Cache::set($coverName,'queue',1200); $args = array($cachePath,$path,$coverName,$item['width']); $desc = '[fileThumb.coverMake]:size='.$file['size'].';cover='.$coverName.';name='.$file['name'].';path='.$file['path']; TaskQueue::add('fileThumbPlugin.coverMake',$args,$desc,$coverName); } // jpg等图片,有该字段时,前端不自动获取图片缩略图,统一通过此插件转换; if(in_array($file['ext'],$supportView) && !isset($file['fileThumb'])){$file['fileThumbDisable'] = 1;} };unset($file); // trace_log([$needMake,$data,$coverList]); return $data; } // 批量查询缓存图片是否存在; private function checkCoverExists($cachePath,$coverList,&$needMake){ if(!$cachePath || !count($coverList)){return $coverList;} $sourceID = kodIO::sourceID($cachePath); $coverArr = array(); foreach($coverList as $items){ foreach($items as $item){$coverArr[] = $item['cover'];} } $where = array('parentID'=>$sourceID,'name'=>array('in',$coverArr)); $lists = Model("Source")->field('sourceID,name')->where($where)->select(); $lists = array_to_keyvalue($lists,'name','sourceID'); foreach($coverList as $i=>$items){ foreach($items as $k=>$item){ if(isset($lists[$item['cover']])){$coverList[$i][$k]['sourceID'] = $lists[$item['cover']];} } if(!isset($coverList[$i]['fileThumb']['sourceID'])){$needMake++;} } return $coverList; } // 单个文件属性处理; public function itemParse($file){ if(strtolower(ACT) != 'pathinfo' || $file['type'] == 'folder'){return $file;} $data = $this->listParse(array('folderList'=>array(),'fileList'=>array($file))); return $data['fileList'][0]; } public function cover(){ $path = $this->filePath($this->in['path'],false); $width = intval($this->in['width']); $width = ($width && $width > 1000) ? 1200:250; $file = IO::info($path); $fileHash = KodIO::hashPath($file); $coverName = "cover_".$fileHash."_{$width}.png"; $result = $this->coverMake($this->cachePath,$file['path'],"cover_".$fileHash."_{$width}.png",$width); if($width == 1200){$this->coverMake($this->cachePath,$file['path'],"cover_".$fileHash."_250.png",250);} $sourceID = IO::fileNameExist($this->cachePath,$coverName); // pr(IO::Info(kodIO::make($sourceID)));exit; if($sourceID){IO::fileOut(kodIO::make($sourceID));exit;} echo $result; } //linux 注意修改获取bin文件的权限问题; public function check(){ Cache::remove('fileThumb.getFFmpeg'); Cache::remove('fileThumb.getConvert'); if(isset($_GET['action']) && $_GET['action'] == 'stopAll'){ // 清除所有任务; @include_once($this->pluginPath.'lib/VideoResize.class.php'); @include_once($this->pluginPath.'lib/TaskConvert.class.php'); $video = new videoResize(); $video->stopAll(); $video->log("Success !"); return; } if(isset($_GET['check'])){ $convert = $this->getConvert(); $ffmpeg = $this->getFFmpegFind(); $ffmpegSupport = $ffmpeg ? $this->ffmpegSupportCheck($ffmpeg) : false; $result = $convert && $ffmpeg && $ffmpegSupport; if($ffmpeg && !$this->ffmpegSupportCheck($ffmpeg)){$result = false;} $message = $result ? 'ok;': LNG('fileThumb.check.faild').'
'; if(!$result){ $error = ''; $check = array('shell_exec','proc_open','proc_close','exec'); foreach ($check as $method) { if(!function_exists($method)){$error .= $method .' ';} } if($error){$message = $message.'['.trim($error).'] is disabled(please allow it)
';} } if(!$convert){ $message .= "$convert convert ".LNG('fileThumb.check.error').";
"; } if(!$ffmpeg){ $message .= "$ffmpeg ffmpeg".LNG('fileThumb.check.error').";
"; } if($ffmpeg && !$ffmpegSupport){ $message .= 'ffmpeg not support muxer:image2 or libx264; please install again!'; } show_json($message,$result); } include($this->pluginPath.'static/check.html'); } // 标清视频; public function videoSmall(){ if(!$this->getFFmpeg()) return; @include_once($this->pluginPath.'lib/VideoResize.class.php'); @include_once($this->pluginPath.'lib/TaskConvert.class.php'); $video = new videoResize(); $video->start($this); } public function videoPreview(){ if(!$this->getFFmpeg()) return; @include_once($this->pluginPath.'lib/VideoResize.class.php'); $video = new videoResize(); $video->videoPreview($this); } // 本地文件:local,io-local, {{kod-local}}全量生成; // 远端:(ftp,oss等对象存储) public function coverMake($cachePath,$path,$coverName,$size){ if(IO::fileNameExist($cachePath,$coverName)){return 'exists;';} if(!is_dir(TEMP_FILES)){mk_dir(TEMP_FILES);} $info = IO::info($path);$ext = $info['ext']; $thumbFile = TEMP_FILES . $coverName; $localFile = $this->localFile($path); $movie = '3gp,avi,mp4,m4v,mov,mpg,mpeg,mpe,mts,m2ts,wmv,ogv,webm,vob,flv,f4v,mkv,rmvb,rm'; $isVideo = in_array($ext,explode(',',$movie)); // 过短的视频封面图,不指定时间; $videoThumbTime = true; if( $isVideo && is_array($info['fileInfoMore']) && isset($info['fileInfoMore']['playtime']) && floatval($info['fileInfoMore']['playtime']) <= 3 ){ $videoThumbTime = false; } if($isVideo){ // 不是本地文件; 切片后获取:mp4,mov,mpg,webm,f4v,ogv,avi,mkv,wmv;(部分失败) if(!$localFile){ $localTemp = $thumbFile.'.'.$ext; $localFile = $localTemp; file_put_contents($localTemp,IO::fileSubstr($path,0,1024*600)); } $this->thumbVideo($localFile,$thumbFile,$videoThumbTime); } else { if(!$localFile){$localFile = $localTemp = $this->pluginLocalFile($path);} if($ext == 'ttf'){ $this->thumbFont($localFile,$thumbFile,$size); }else{ $this->thumbImage($localFile,$thumbFile,$size,$ext); } } if($localTemp){@unlink($localTemp);} if(@file_exists($thumbFile) && @is_file($thumbFile) && filesize($thumbFile) > 0){ Cache::remove($coverName); $destFile = IO::move($thumbFile,$cachePath); $pathInfo = KodIO::parse($destFile); if($pathInfo['type'] == KodIO::KOD_SOURCE){ Model('Source')->setDesc($pathInfo['id'],$path); //write_log(get_caller_info(),'test'); } return $destFile; } Cache::set($coverName,'no',600); del_file($thumbFile); $this->log('cover=makeError:'.$localFile.';temp='.$thumbFile,'fileThumb'); return 'convert error! localFile='.get_path_this($localFile); } private function getSize($size){ $sizeAllow = array(250,1200,3000); for ($i=0;$i $sizeAllow[$i - 1] && $size <= $sizeAllow[$i]){ $size = $sizeAllow[$i];break; }else if($i == count($sizeAllow) - 1 && $size > $sizeAllow[$i]){ $size = $sizeAllow[$i];break; } } return $size; } // 获取文件 hash public function localFile($path){ $io = IO::init($path); $pathParse = KodIO::parse($path); if(!$pathParse['type']) return $path; if(is_array($io->pathParse) && isset($io->pathParse['truePath'])){ //协作分享处理; if(file_exists($io->pathParse['truePath'])) return $io->pathParse['truePath']; return false; } $fileInfo = IO::info($path); if($fileInfo['fileID']){ $tempInfo = Model('File')->fileInfo($fileInfo['fileID']); $fileInfo = IO::info($tempInfo['path']); $pathParse = KodIO::parse($tempInfo['path']); } $parent = array('path'=>'{userFav}/'); $fileInfo = Action('explorer.listDriver')->parsePathIO($fileInfo,$parent); $this->ioFileInfo = array( 'path' => $path, 'ioDriver' => strtolower($fileInfo['ioDriver']) ); if($fileInfo['ioDriver'] == 'Local' && $fileInfo['ioBasePath']){ $base = rtrim($fileInfo['ioBasePath'],'/'); if(substr($base,0,2) == './') { $base = substr_replace($base, BASIC_PATH, 0, 2); } return $base . '/' . ltrim($pathParse['param'], '/'); } return false; } private function thumbFont($fontFile,$cacheFile,$maxSize){ $textMore = ' 可道云在线云盘 汉体书写信息技术标准相容 档案下载使用界面简单 支援服务升级资讯专业制作 创意空间快速无线上网 AaBbCc 0123456789AaBbCc ㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩'; $textMore = str_replace("\t",' ',$textMore); $text = "字体ABC";$size = 200; if($maxSize >= 500){ $text = $textMore; $size = 800; } $im = imagecreatetruecolor($size,$size); imagefill($im,0,0,imagecolorallocate($im,255,255,255)); $color = imagecolorallocate($im,10,10,10); imagefttext($im,32,0,10,100,$color,$fontFile,$text); imagejpeg($im,$cacheFile);//生成图片 imagedestroy($im); } private function thumbVideo($file,$cacheFile,$videoThumbTime){ $command = $this->getFFmpeg(); if(!$command){ echo "Ffmpeg ".LNG("fileThumb.check.notFound"); return false; } $tempPath = $cacheFile; if($GLOBALS['config']['systemOS'] == 'linux' && is_writable('/tmp/')){ mk_dir('/tmp/fileThumb'); $tempPath = '/tmp/fileThumb/'.rand_string(15).'.jpg'; } $maxWidth = 800; $timeAt = $videoThumbTime ? '-ss 00:00:03' : ''; $this->setLctype($file,$tempPath); $script = $command.' -i '.escapeShell($file).' -y -f image2 '.$timeAt.' -vframes 1 '.escapeShell($tempPath).' 2>&1'; $out = shell_exec($script); if(!file_exists($tempPath)) { if ($this->thumbVideoByLink($cacheFile)) return; return $this->log('video thumb error,'.$out.';cmd='.$script); } move_path($tempPath,$cacheFile); $cm = new ImageThumb($cacheFile,'file'); $cm->prorate($cacheFile,$maxWidth,$maxWidth); } // 对象存储通过链接获取缩略图——当前仅支持OSS // https://help.aliyun.com/zh/oss/user-guide/video-snapshots? private function thumbVideoByLink($cacheFile) { if (!isset($this->ioFileInfo['ioDriver']) || $this->ioFileInfo['ioDriver'] != 'oss') return; // 获取缩略图链接 $options = array('x-oss-process' => 'video/snapshot,t_10000,f_jpg,w_250,m_fast'); $link = IO::link($this->ioFileInfo['path'], $options); if (!$link) return; // 写入文件——视频缩略图收费,写入文件,避免反复调用(截帧数*(0.1/1k)/1000) $content = curl_get_contents($link); if (!$content) return; file_put_contents($cacheFile, $content); return @file_exists($cacheFile) ? true : false; } // 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 private function thumbImage($file,$cacheFile,$maxSize,$ext){ $this->thumbImageCreate($file,$cacheFile,$maxSize,$ext); $isResize = explode(',','gif,png,bmp,jpe,jpeg,jpg,webp,heic'); if(in_array($ext,$isResize)) return; ImageThumb::createThumb($cacheFile,$cacheFile,$maxSize,$maxSize); } public function thumbImageCreate($file,$cacheFile,$maxSize,$ext){ $command = $this->getConvert(); if(!$command){ echo "ImageMagick ".LNG("fileThumb.check.notFound"); return false; } $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 'heic':$param = "-auto-orient -resize ".$size;break; default: $dng = 'dng,cr2,erf,raf,kdc,dcr,mrw,nrw,nef,orf,pef,x3f,srf,arw,sr2'; $dng = $dng.',3fr,crw,dcm,fff,iiq,mdc,mef,mos,plt,ppm,raw,rw2,srw,tst'; if(in_array($ext,explode(',',$dng))){ $param = "-resize ".$size; //$file = 'rgb:'.$file.'[0]'; } break; } //linux下$cacheFile不可写问题,先生成到/tmp下;再移动出来 $tempPath = $cacheFile; if($GLOBALS['config']['systemOS'] == 'linux' && is_writable('/tmp/')){ mk_dir('/tmp/fileThumb'); if(is_writable('/tmp/fileThumb')){ // 可能有不可写的情况; $tempPath = '/tmp/fileThumb/'.$tempName; } } $this->setLctype($file,$tempPath); $script = $command.' '.$param.' '.escapeShell($file).' '.escapeShell($tempPath).' 2>&1'; $out = shell_exec($script); if(!file_exists($tempPath)) return $this->log('image thumb error:'.$out.';cmd='.$script); move_path($tempPath,$cacheFile); return true; } // 设置地区字符集,避免中文被过滤 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"); } } public function getFFmpeg(){ return $this->getCall('fileThumb.getFFmpeg',600,array($this,'getFFmpegNow')); } public function getConvert(){ return $this->getCall('fileThumb.getConvert',600,array($this,'getConvertNow')); } // Cache::getCall private function getCall($key,$timeout,$call,$args = array()){ $result = Cache::get($key); if($result || $result === '') return $result; $result = call_user_func_array($call,$args); $result = $result ? $result : ''; Cache::set($key,$result,$timeout); return $result; } public function ffmpegSupportCheck($ffmpeg){ $out = shell_exec($ffmpeg.' -v 2>&1'); if(strstr($out,'--disable-muxer=image2')){ $this->log('ffmpeg support error. '.$out); return false; } return true; } public function getFFmpegNow(){ $ffmpeg = $this->getFFmpegFind(); if(!$ffmpeg) return false; return $this->ffmpegSupportCheck($ffmpeg) ? $ffmpeg:false; } public function getFFmpegFind(){ $check = 'options'; $config = $this->getConfig(); if( $this->checkBin($config['ffmpegBin'],$check) ){ return $config['ffmpegBin']; } $result = $this->guessBinPath('ffmpeg',$check); if($result){return $result;} $findMore = array( '/imagemagick/ffmpeg', '/imagemagick/bin/ffmpeg', '/ImageMagick/ffmpeg.exe', '/ImageMagick-7.0.7-Q8/ffmpeg.exe', ); foreach ($findMore as $value) { $result = $this->guessBinPath($value,$check); if($result){return $result;} } return false; } public function getConvertNow(){ $check = 'options'; $config = $this->getConfig(); if( $this->checkBin($config['imagickBin'],$check) ){ return $config['imagickBin']; } $result = $this->guessBinPath('convert',$check); if($result){return $result;} $findMore = array( '/imagemagick/convert', '/imagemagick/bin/convert', '/ImageMagick/convert.exe', '/ImageMagick-7.0.7-Q8/convert.exe', ); foreach ($findMore as $value) { $result = $this->guessBinPath($value,$check); if($result){return $result;} } return false; } /** * 查找可执行文件命令 * https://github.com/taligentx/ee204/blob/master/admin/SCRIPT_server_tools_pathguess.php * @param [type] $bin 命令文件 * @param [type] $check 找到文件后执行,结果中匹配的字符串 * @return [type] 可执行命令路径 */ private function guessBinPath($bin,$check){ $array = array( "/bin/", "/usr/local/bin/", "/usr/bin/", "/usr/sbin/", "/usr/local/", "/local/bin/", "C:/Program Files/", "C:/Program Files (x86)/", "C:/", ); $findArray = array(); foreach ($array as $value) { if(file_exists($value.$bin)){ $file = $value.$bin; if(strstr($file,' ')){ $file = '"'.$file.'"'; } $findArray[] = $file; } } if(!strstr($bin,'/')){ $findArray[] = $bin; } if(count($findArray) > 0){ foreach ($findArray as $file) { if( $this->checkBin($file,$check) ){ //var_dump($file,$findArray,$check,shell_exec($file.' --help'));exit; return $file; } } } return false; } private function checkBin($bin,$check){ if(!function_exists('shell_exec')) { $this->log('shell_exec function is disabled.'); return false; } $result = shell_exec($bin.' --help'); if (stripos($result,$check) > 0) return true; $out = shell_exec($bin.' --help 2>&1'); $this->log('imagick env error:'.$out.';cmd='.$bin.' --help 2>&1'); return false; } // 调试模式 public function log($log, $cmd = ''){ // $config = $this->getConfig(); //if(!$config['debug']) return; write_log($log,'fileThumb'); } }