PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.18
ووسلام – همگام سازی ووکامرس و باسلام v1.10.18
1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 1.8.7 1.8.4 All 51 releases
sync-basalam / includes / Services / ImageFormatNormalizer.php

ImageFormatNormalizer.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.18, at includes/Services/ImageFormatNormalizer.php

273 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam\Services;
4
5 use SyncBasalam\Logger\Logger;
6
7 defined('ABSPATH') || exit;
8
9 /**
10 * Basalam only accepts jpg, jpeg, png, webp, gif, bmp and jfif images.
11 * Media libraries can hold AVIF or other unsupported image files, and the media upload-request
12 * endpoint rejects them with 422 «نوع MIME پشتیبانی ن�
13 ی‌شود».
14 * Those files are transcoded to JPEG before the upload starts.
15 */
16 class ImageFormatNormalizer
17 {
18 public const SUPPORTED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'jfif'];
19
20 private const SUPPORTED_MIME_TYPES = [
21 'image/jpeg',
22 'image/pjpeg',
23 'image/png',
24 'image/webp',
25 'image/gif',
26 'image/bmp',
27 'image/x-ms-bmp',
28 'image/x-windows-bmp',
29 ];
30
31 private const TARGET_MIME = 'image/jpeg';
32 private const QUALITY_STEPS = [82, 70, 60];
33 private const MAX_DIMENSION = 2500;
34
35 /**
36 * Returns the path of a converted temporary JPEG file, or null when the file
37 * is already in a format Basalam accepts and can be uploaded as is.
38 */
39 public function normalize(string $filePath, int $maxSize): ?string
40 {
41 if (!file_exists($filePath)) return null;
42
43 $mimeType = $this->detectMimeType($filePath);
44 if ($this->isSupported($filePath, $mimeType)) return null;
45
46 $converted = $this->convertToJpeg($filePath, $mimeType, $maxSize);
47
48 if ($converted === null) {
49 Logger::error('تبدیل تصویر به فر�
50 ت �
51 ورد پذیرش باسلا�
52 نا�
53 وفق بود.', [
54 'file' => basename($filePath),
55 'mime_type' => $mimeType,
56 'imagick' => class_exists('Imagick'),
57 'gd_avif' => function_exists('imagecreatefromavif'),
58 ]);
59
60 $detectedType = $mimeType ?: strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
61
62 throw new \RuntimeException(esc_html(
63 'فر�
64 ت این تصویر (' . $detectedType . ') �
65 ورد پذیرش باسلا�
66 نیست'
67 . ' و تبدیل خودکار آن روی سرور ش�
68 ا �
69
70 کن نشد. لطفاً تصویر را با فر�
71 ت JPG یا WEBP در سایت بارگذاری کنید'
72 . ' یا از هاست خود بخواهید پشتیبانی فر�
73 ت تصویر را در Imagick یا GD فعال کند.'
74 ));
75 }
76
77 Logger::info('تصویر برای آپلود به باسلا�
78 به فر�
79 ت JPEG تبدیل شد.', [
80 'file' => basename($filePath),
81 'mime_type' => $mimeType,
82 ]);
83
84 return $converted;
85 }
86
87 private function convertToJpeg(string $filePath, string $mimeType, int $maxSize): ?string
88 {
89 $converters = ['convertWithImageEditor', 'convertWithImagick', 'convertWithGd'];
90
91 foreach ($converters as $converter) {
92 foreach (self::QUALITY_STEPS as $quality) {
93 $target = $this->makeTempPath();
94 $result = $this->$converter($filePath, $target, $mimeType, $quality);
95
96 if ($result === null) {
97 $this->deleteFile($target);
98 break;
99 }
100
101 $size = filesize($result);
102 if ($size !== false && $size > 0 && $size <= $maxSize) return $result;
103
104 $this->deleteFile($result);
105 if ($result !== $target) $this->deleteFile($target);
106 }
107 }
108
109 return null;
110 }
111
112 private function convertWithImageEditor(string $source, string $target, string $mimeType, int $quality): ?string
113 {
114 if (!function_exists('wp_get_image_editor')) return null;
115
116 $args = $mimeType !== '' ? ['mime_type' => $mimeType] : [];
117 $editor = wp_get_image_editor($source, $args);
118
119 if (is_wp_error($editor)) return null;
120
121 $editor->set_quality($quality);
122
123 $size = $editor->get_size();
124 $width = isset($size['width']) ? (int) $size['width'] : 0;
125 $height = isset($size['height']) ? (int) $size['height'] : 0;
126
127 if ($width > self::MAX_DIMENSION || $height > self::MAX_DIMENSION) {
128 $editor->resize(self::MAX_DIMENSION, self::MAX_DIMENSION, false);
129 }
130
131 $saved = $editor->save($target, self::TARGET_MIME);
132
133 if (is_wp_error($saved) || !is_array($saved)) return null;
134
135 // An image_editor_output_format filter can force another extension, so trust the returned path.
136 $savedPath = !empty($saved['path']) ? (string) $saved['path'] : $target;
137 $savedExtension = strtolower(pathinfo($savedPath, PATHINFO_EXTENSION));
138
139 if (!file_exists($savedPath) || !in_array($savedExtension, self::SUPPORTED_EXTENSIONS, true)) {
140 $this->deleteFile($savedPath);
141 return null;
142 }
143
144 return $savedPath;
145 }
146
147 private function convertWithImagick(string $source, string $target, string $mimeType, int $quality): ?string
148 {
149 if (!class_exists('Imagick')) return null;
150
151 try {
152 $imagick = new \Imagick();
153 $imagick->readImage($source);
154 $imagick->setImageBackgroundColor(new \ImagickPixel('white'));
155
156 $flattened = $imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
157 $imagick->clear();
158 $imagick = $flattened;
159
160 $width = (int) $imagick->getImageWidth();
161 $height = (int) $imagick->getImageHeight();
162
163 if ($width > self::MAX_DIMENSION || $height > self::MAX_DIMENSION) {
164 $ratio = self::MAX_DIMENSION / max($width, $height);
165 $imagick->resizeImage(
166 max(1, (int) round($width * $ratio)),
167 max(1, (int) round($height * $ratio)),
168 \Imagick::FILTER_LANCZOS,
169 1
170 );
171 }
172
173 $imagick->setImageFormat('jpeg');
174 $imagick->setImageCompressionQuality($quality);
175 $imagick->stripImage();
176 $imagick->writeImage($target);
177 $imagick->clear();
178 } catch (\Throwable $e) {
179 Logger::debug('تبدیل تصویر با Imagick نا�
180 وفق بود: ' . $e->getMessage(), ['file' => basename($source)]);
181 return null;
182 }
183
184 return file_exists($target) ? $target : null;
185 }
186
187 private function convertWithGd(string $source, string $target, string $mimeType, int $quality): ?string
188 {
189 if (!function_exists('imagejpeg') || !function_exists('imagecreatefromstring')) return null;
190
191 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local file read for image conversion; WP_Filesystem cannot feed GD.
192 $contents = file_get_contents($source);
193 if ($contents === false) return null;
194
195 $image = @imagecreatefromstring($contents);
196 unset($contents);
197
198 if (!$image) return null;
199
200 $canvas = null;
201
202 try {
203 $width = imagesx($image);
204 $height = imagesy($image);
205
206 $ratio = max($width, $height) > self::MAX_DIMENSION ? self::MAX_DIMENSION / max($width, $height) : 1;
207 $targetWidth = max(1, (int) round($width * $ratio));
208 $targetHeight = max(1, (int) round($height * $ratio));
209
210 $canvas = imagecreatetruecolor($targetWidth, $targetHeight);
211 if (!$canvas) return null;
212
213 $white = imagecolorallocate($canvas, 255, 255, 255);
214 imagefilledrectangle($canvas, 0, 0, $targetWidth, $targetHeight, $white);
215 imagecopyresampled($canvas, $image, 0, 0, 0, 0, $targetWidth, $targetHeight, $width, $height);
216
217 $saved = imagejpeg($canvas, $target, $quality);
218 if (!$saved) return null;
219 } finally {
220 if ($canvas) imagedestroy($canvas);
221 imagedestroy($image);
222 }
223
224 return file_exists($target) ? $target : null;
225 }
226
227 private function detectMimeType(string $filePath): string
228 {
229 if (function_exists('wp_get_image_mime')) {
230 $mimeType = wp_get_image_mime($filePath);
231 if (is_string($mimeType) && $mimeType !== '') return strtolower($mimeType);
232 }
233
234 if (function_exists('finfo_open')) {
235 $finfo = finfo_open(FILEINFO_MIME_TYPE);
236 if ($finfo) {
237 $mimeType = finfo_file($finfo, $filePath);
238 finfo_close($finfo);
239 if (is_string($mimeType) && $mimeType !== '' && $mimeType !== 'application/octet-stream') {
240 return strtolower($mimeType);
241 }
242 }
243 }
244
245 $fileType = wp_check_filetype($filePath);
246
247 return !empty($fileType['type']) ? strtolower((string) $fileType['type']) : '';
248 }
249
250 private function isSupported(string $filePath, string $mimeType): bool
251 {
252 if (in_array($mimeType, self::SUPPORTED_MIME_TYPES, true)) return true;
253
254 // A known image type that is not on the list (avif, heic, tiff, ...) always needs conversion.
255 if (strpos($mimeType, 'image/') === 0) return false;
256
257 // Unknown mime type: fall back to the extension so nothing that worked before is converted.
258 $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
259
260 return in_array($extension, self::SUPPORTED_EXTENSIONS, true);
261 }
262
263 private function makeTempPath(): string
264 {
265 return trailingslashit(sys_get_temp_dir()) . uniqid('basalam_image_', true) . '.jpg';
266 }
267
268 private function deleteFile(string $path): void
269 {
270 if ($path !== '' && file_exists($path)) unlink($path);
271 }
272 }
273