diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index 9ce03c8fc..8c55b826c 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -3,9 +3,11 @@
namespace App\Exceptions;
use App\Module\Base;
+use App\Module\Image;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\Log;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
class Handler extends ExceptionHandler
@@ -51,6 +53,11 @@ class Handler extends ExceptionHandler
*/
public function render($request, Throwable $e)
{
+ if ($e instanceof NotFoundHttpException) {
+ if ($result = $this->ImagePathHandler($request)) {
+ return $result;
+ }
+ }
if ($e instanceof ApiException) {
return response()->json(Base::retError($e->getMessage(), $e->getData(), $e->getCode()));
} elseif ($e instanceof ModelNotFoundException) {
@@ -78,4 +85,133 @@ class Handler extends ExceptionHandler
parent::report($e);
}
}
+
+ /**
+ * 图片路径处理
+ * @param $request
+ * @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse|null
+ */
+ private function ImagePathHandler($request)
+ {
+ $path = $request->path();
+
+ // 处理图片
+ $pattern = '/^(uploads\/.*\.(png|jpg|jpeg))\/crop\/([^\/]+)$/';
+ if (preg_match($pattern, $path, $matches)) {
+ // 获取参数
+ $file = $matches[1];
+ $ext = $matches[2];
+ $rules = preg_replace('/\s+/', '', $matches[3]);
+ $rules = str_replace(['=', '&'], [':', ','], $rules);
+ $rules = explode(',', $rules);
+ if (empty($rules)) {
+ return null;
+ }
+
+ // 提取年月
+ $Ym = date("Ym");
+ if (preg_match('/\/(\d{6})\//', $file, $ms)) {
+ $Ym = $ms[1];
+ }
+
+ // 文件存在直接返回
+ $dirName = str_replace(['/', '.'], '_', $file);
+ $fileName = str_replace([':', ','], ['-', '_'], implode(',', $rules)) . '.' . $ext;
+ $savePath = public_path('uploads/tmp/crop/' . $Ym . '/' . $dirName . '/' . $fileName);
+ if (file_exists($savePath)) {
+ // 设置头部声明图片缓存
+ return response()->file($savePath, [
+ 'Pragma' => 'public',
+ 'Cache-Control' => 'max-age=1814400',
+ 'Expires' => gmdate('D, d M Y H:i:s', time() + 1814400) . ' GMT',
+ 'Last-Modified' => gmdate('D, d M Y H:i:s', filemtime($savePath)) . ' GMT',
+ 'ETag' => md5_file($savePath)
+ ]);
+ }
+
+ // 文件不存在处理
+ $sourcePath = public_path($file);
+ if (!file_exists($sourcePath)) {
+ return null;
+ }
+
+ // 判断删除多余文件
+ $saveDir = dirname($savePath);
+ if (is_dir($saveDir)) {
+ $items = glob($saveDir . '/*');
+ if (count($items) > 5) {
+ usort($items, function ($a, $b) {
+ return filemtime($b) - filemtime($a);
+ });
+ $itemsToDelete = array_slice($items, 5);
+ foreach ($itemsToDelete as $item) {
+ if (is_file($item)) {
+ unlink($item);
+ }
+ }
+ }
+ } else {
+ Base::makeDir($saveDir);
+ }
+
+ // 处理图片
+ try {
+ $handle = 0;
+ $image = new Image($sourcePath);
+ foreach ($rules as $rule) {
+ if (!str_contains($rule, ':')) {
+ continue;
+ }
+ [$type, $value] = explode(':', $rule);
+ if (!in_array($type, ['ratio', 'size', 'percentage', 'cover', 'contain'])) {
+ continue;
+ }
+ switch ($type) {
+ // 按比例裁剪
+ case 'ratio':
+ if (is_numeric($value)) {
+ $image->ratioCrop($value);
+ $handle++;
+ }
+ break;
+
+ // 按尺寸缩放
+ case 'size':
+ $size = Base::newIntval(explode('x', $value));
+ if (count($size) === 2) {
+ $image->resize($size[0], $size[1]);
+ $handle++;
+ }
+ break;
+
+ // 按尺寸缩放
+ case 'percentage':
+ case 'cover':
+ case 'contain':
+ $size = Base::newIntval(explode('x', $value));
+ if (count($size) === 2) {
+ $image->thumb($size[0], $size[1], $type);
+ $handle++;
+ }
+ break;
+ }
+ }
+ if ($handle > 0) {
+ $image->saveTo($savePath);
+ Image::compressImage($savePath, null, 80);
+ return response()->file($savePath, [
+ 'Pragma' => 'public',
+ 'Cache-Control' => 'max-age=1814400',
+ 'Expires' => gmdate('D, d M Y H:i:s', time() + 1814400) . ' GMT',
+ 'Last-Modified' => gmdate('D, d M Y H:i:s', filemtime($savePath)) . ' GMT',
+ 'ETag' => md5_file($savePath)
+ ]);
+ } else {
+ $image->destroy();
+ }
+ } catch (\ImagickException) { }
+ }
+
+ return null;
+ }
}
diff --git a/app/Models/File.php b/app/Models/File.php
index eb3a3b3d3..b35d674a6 100644
--- a/app/Models/File.php
+++ b/app/Models/File.php
@@ -245,7 +245,7 @@ class File extends AbstractModel
}
}
//
- $path = 'uploads/tmp/' . date("Ym") . '/';
+ $path = 'uploads/tmp/file/' . date("Ym") . '/';
$data = Base::upload([
"file" => Request::file('files'),
"type" => 'more',
diff --git a/app/Module/Base.php b/app/Module/Base.php
index 850fb5151..170ff0027 100755
--- a/app/Module/Base.php
+++ b/app/Module/Base.php
@@ -2507,7 +2507,7 @@ class Base
*/
public static function isThumb($file): bool
{
- return preg_match('/_thumb\.(jpg|jpeg|png)$/', $file);
+ return preg_match('/_thumb\.(png|jpg|jpeg)$/', $file);
}
/**
@@ -2535,7 +2535,8 @@ class Base
*/
public static function thumbRestore($file): string
{
- return preg_replace('/_thumb\.(jpg|jpeg|png)$/', '', $file);
+ $file = preg_replace('/_thumb\.(png|jpg|jpeg)$/', '', $file);
+ return preg_replace('/\/crop\/([^\/]+)$/', '', $file);
}
/**
diff --git a/app/Module/Image.php b/app/Module/Image.php
index 97f895f73..e0d8f6384 100644
--- a/app/Module/Image.php
+++ b/app/Module/Image.php
@@ -58,11 +58,11 @@ class Image
/**
* 按比例裁剪
- * @param int $ratio
+ * @param float $ratio
* @return $this
* @throws \ImagickException
*/
- public function ratioCrop(int $ratio = 0): static
+ public function ratioCrop(float $ratio = 0): static
{
if ($ratio === 0) {
return $this;
@@ -77,7 +77,7 @@ class Image
$newHeight = $height;
} elseif ($height > $width * $ratio) {
$newWidth = $width;
- $newHeight = $width * 3;
+ $newHeight = $width * $ratio;
} else {
return $this;
}
@@ -201,6 +201,15 @@ class Image
$this->image->destroy();
}
+ /**
+ * 销毁对象
+ * @return void
+ */
+ public function destroy()
+ {
+ $this->image->destroy();
+ }
+
/** ******************************************************************************/
/** ******************************************************************************/
/** ******************************************************************************/
diff --git a/resources/assets/js/functions/common.js b/resources/assets/js/functions/common.js
index b047bef2c..b1bd05716 100755
--- a/resources/assets/js/functions/common.js
+++ b/resources/assets/js/functions/common.js
@@ -1067,11 +1067,13 @@ const timezone = require("dayjs/plugin/timezone");
* 等比缩放尺寸
* @param width
* @param height
- * @param maxWidth
- * @param maxHeight
+ * @param maxW
+ * @param maxH
* @returns {{width, height}|{width: number, height: number}}
*/
- scaleToScale(width, height, maxWidth, maxHeight) {
+ scaleToScale(width, height, maxW, maxH = undefined) {
+ const maxWidth = maxW;
+ const maxHeight = typeof maxH === "undefined" ? maxW : maxH;
let tempWidth;
let tempHeight;
if (width > 0 && height > 0) {
diff --git a/resources/assets/js/functions/web.js b/resources/assets/js/functions/web.js
index c380a3dc5..b30e5ef0f 100755
--- a/resources/assets/js/functions/web.js
+++ b/resources/assets/js/functions/web.js
@@ -252,10 +252,22 @@ import {MarkdownPreview} from "../store/markdown";
const widthMatch = res.match("width=\"(\\d+)\""),
heightMatch = res.match("height=\"(\\d+)\"");
if (widthMatch && heightMatch) {
- const width = parseInt(widthMatch[1]),
- height = parseInt(heightMatch[1]),
- maxSize = 40;
- const scale = $A.scaleToScale(width, height, maxSize, maxSize);
+ const data = {
+ width: parseInt(widthMatch[1]),
+ height: parseInt(heightMatch[1]),
+ maxSize: 40,
+ src
+ }
+ const ratioExceed = $A.imageRatioExceed(data.width, data.height, 2)
+ if (ratioExceed > 0 && /\.(png|jpg|jpeg)$/.test(data.src)) {
+ src = $A.thumbRestore(data.src) + `/crop/ratio:${ratioExceed},percentage:80x0`
+ if (data.width > data.height) {
+ data.width = data.height * ratioExceed;
+ } else {
+ data.height = data.width * ratioExceed;
+ }
+ }
+ const scale = $A.scaleToScale(data.width, data.height, data.maxSize);
imgClassName = `${imgClassName}" style="width:${scale.width}px;height:${scale.height}px`
}
return `[image:${src}]`
@@ -274,10 +286,11 @@ import {MarkdownPreview} from "../store/markdown";
if (imgClassName) {
text = text.replace(/\[image:(.*?)\]/g, ``)
text = text.replace(/\{\{RemoteURL\}\}/g, this.apiUrl('../'))
- }
- const tmpText = text.substring(0, 30)
- if (tmpText.length < text.length) {
- text = tmpText + '...'
+ } else {
+ const tmpText = text.substring(0, 30)
+ if (tmpText.length < text.length) {
+ text = tmpText + '...'
+ }
}
return text
},
@@ -339,13 +352,30 @@ import {MarkdownPreview} from "../store/markdown";
const widthMatch = res.match(widthReg),
heightMatch = res.match(heightReg);
if (widthMatch && heightMatch) {
- const width = parseInt(widthMatch[1]),
- height = parseInt(heightMatch[1]),
- maxSize = res.indexOf("emoticon") > -1 ? 150 : 220; // 跟css中的设置一致
- const scale = $A.scaleToScale(width, height, maxSize, maxSize);
- const value = res
- .replace(widthReg, `original-width="${width}"`)
- .replace(heightReg, `original-height="${height}" style="width:${scale.width}px;height:${scale.height}px"`)
+ const data = {
+ res,
+ width: parseInt(widthMatch[1]),
+ height: parseInt(heightMatch[1]),
+ maxSize: res.indexOf("emoticon") > -1 ? 150 : 220, // 跟css中的设置一致
+ }
+ if (data.maxSize === 220) {
+ const ratioExceed = $A.imageRatioExceed(data.width, data.height)
+ if (ratioExceed > 0) {
+ const srcMatch = res.match(/src=(["'])(([^'"]*)\.(png|jpg|jpeg))\1/);
+ if (srcMatch) {
+ data.res = data.res.replace(srcMatch[2], $A.thumbRestore(srcMatch[2]) + `/crop/ratio:${ratioExceed},percentage:320x0`)
+ if (data.width > data.height) {
+ data.width = data.height * ratioExceed;
+ } else {
+ data.height = data.width * ratioExceed;
+ }
+ }
+ }
+ }
+ const scale = $A.scaleToScale(data.width, data.height, data.maxSize);
+ const value = data.res
+ .replace(widthReg, `original-width="${data.width}"`)
+ .replace(heightReg, `original-height="${data.height}" style="width:${scale.width}px;height:${scale.height}px"`)
text = text.replace(res, value)
} else {
text = text.replace(res, `