Image.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. <?php
  2. namespace App\Util\Media;
  3. use App\Media;
  4. use App\Services\StatusService;
  5. use Cache;
  6. use Intervention\Image\Encoders\JpegEncoder;
  7. use Intervention\Image\Encoders\PngEncoder;
  8. use Intervention\Image\Encoders\WebpEncoder;
  9. use Intervention\Image\ImageManager;
  10. use Log;
  11. use Storage;
  12. class Image
  13. {
  14. public $square;
  15. public $landscape;
  16. public $portrait;
  17. public $thumbnail;
  18. public $orientation;
  19. public $acceptedMimes = [
  20. 'image/png',
  21. 'image/jpeg',
  22. 'image/jpg',
  23. 'image/webp',
  24. 'image/avif',
  25. 'image/heic',
  26. ];
  27. protected $imageManager;
  28. protected $defaultDisk;
  29. public function __construct()
  30. {
  31. ini_set('memory_limit', config('pixelfed.memory_limit', '1024M'));
  32. $this->square = $this->orientations()['square'];
  33. $this->landscape = $this->orientations()['landscape'];
  34. $this->portrait = $this->orientations()['portrait'];
  35. $this->thumbnail = [
  36. 'width' => 640,
  37. 'height' => 640,
  38. ];
  39. $this->orientation = null;
  40. $this->defaultDisk = config('filesystems.default');
  41. $driver = match (config('image.driver')) {
  42. 'imagick' => \Intervention\Image\Drivers\Imagick\Driver::class,
  43. 'vips' => \Intervention\Image\Drivers\Vips\Driver::class,
  44. default => \Intervention\Image\Drivers\Gd\Driver::class
  45. };
  46. $this->imageManager = new ImageManager(
  47. $driver,
  48. autoOrientation: true,
  49. decodeAnimation: true,
  50. blendingColor: 'ffffff',
  51. strip: true
  52. );
  53. }
  54. public function orientations()
  55. {
  56. return [
  57. 'square' => [
  58. 'width' => 1080,
  59. 'height' => 1080,
  60. ],
  61. 'landscape' => [
  62. 'width' => 1920,
  63. 'height' => 1080,
  64. ],
  65. 'portrait' => [
  66. 'width' => 1080,
  67. 'height' => 1350,
  68. ],
  69. ];
  70. }
  71. public function getAspect($width, $height, $isThumbnail)
  72. {
  73. if ($isThumbnail) {
  74. return [
  75. 'dimensions' => $this->thumbnail,
  76. 'orientation' => 'thumbnail',
  77. ];
  78. }
  79. $aspect = $width / $height;
  80. $orientation = $aspect === 1 ? 'square' :
  81. ($aspect > 1 ? 'landscape' : 'portrait');
  82. $this->orientation = $orientation;
  83. return [
  84. 'dimensions' => $this->orientations()[$orientation],
  85. 'orientation' => $orientation,
  86. 'width_original' => $width,
  87. 'height_original' => $height,
  88. ];
  89. }
  90. public function resizeImage(Media $media)
  91. {
  92. $this->handleResizeImage($media);
  93. }
  94. public function resizeThumbnail(Media $media)
  95. {
  96. $this->handleThumbnailImage($media);
  97. }
  98. public function handleResizeImage(Media $media)
  99. {
  100. $this->handleImageTransform($media, false);
  101. }
  102. public function handleThumbnailImage(Media $media)
  103. {
  104. $this->handleImageTransform($media, true);
  105. }
  106. public function handleImageTransform(Media $media, $thumbnail = false)
  107. {
  108. $path = $media->media_path;
  109. $localFs = config('filesystems.default') === 'local';
  110. if (! in_array($media->mime, $this->acceptedMimes)) {
  111. return;
  112. }
  113. try {
  114. $fileContents = null;
  115. $tempFile = null;
  116. if ($this->defaultDisk === 'local') {
  117. $filePath = storage_path('app/'.$path);
  118. $fileContents = file_get_contents($filePath);
  119. } else {
  120. $fileContents = Storage::disk($this->defaultDisk)->get($path);
  121. }
  122. $fileInfo = pathinfo($path);
  123. $extension = strtolower($fileInfo['extension'] ?? 'jpg');
  124. $outputExtension = $extension;
  125. $metadata = null;
  126. if (! $thumbnail && config('media.exif.database', false) == true) {
  127. try {
  128. if ($this->defaultDisk !== 'local') {
  129. $tempFile = tempnam(sys_get_temp_dir(), 'exif_');
  130. file_put_contents($tempFile, $fileContents);
  131. $exifPath = $tempFile;
  132. } else {
  133. $exifPath = storage_path('app/'.$path);
  134. }
  135. $exif = @exif_read_data($exifPath);
  136. if ($exif) {
  137. $meta = [];
  138. $keys = [
  139. 'FileName',
  140. 'FileSize',
  141. 'FileType',
  142. 'Make',
  143. 'Model',
  144. 'MimeType',
  145. 'ColorSpace',
  146. 'ExifVersion',
  147. 'Orientation',
  148. 'UserComment',
  149. 'XResolution',
  150. 'YResolution',
  151. 'FileDateTime',
  152. 'SectionsFound',
  153. 'ExifImageWidth',
  154. 'ResolutionUnit',
  155. 'ExifImageLength',
  156. 'FlashPixVersion',
  157. 'Exif_IFD_Pointer',
  158. 'YCbCrPositioning',
  159. 'ComponentsConfiguration',
  160. 'ExposureTime',
  161. 'FNumber',
  162. 'ISOSpeedRatings',
  163. 'ShutterSpeedValue',
  164. ];
  165. foreach ($exif as $k => $v) {
  166. if (in_array($k, $keys)) {
  167. $meta[$k] = $v;
  168. }
  169. }
  170. $media->metadata = json_encode($meta);
  171. }
  172. if ($tempFile && file_exists($tempFile)) {
  173. unlink($tempFile);
  174. $tempFile = null;
  175. }
  176. } catch (\Exception $e) {
  177. if ($tempFile && file_exists($tempFile)) {
  178. unlink($tempFile);
  179. }
  180. if (config('app.dev_log')) {
  181. Log::info('EXIF extraction failed: '.$e->getMessage());
  182. }
  183. }
  184. }
  185. $img = $this->imageManager->read($fileContents);
  186. $ratio = $this->getAspect($img->width(), $img->height(), $thumbnail);
  187. $aspect = $ratio['dimensions'];
  188. $orientation = $ratio['orientation'];
  189. if ($thumbnail) {
  190. $img = $img->coverDown(
  191. $aspect['width'],
  192. $aspect['height']
  193. );
  194. } else {
  195. if (
  196. ($ratio['width_original'] > $aspect['width'])
  197. || ($ratio['height_original'] > $aspect['height'])
  198. ) {
  199. $img = $img->scaleDown(
  200. $aspect['width'],
  201. $aspect['height']
  202. );
  203. }
  204. }
  205. $quality = config_cache('pixelfed.image_quality');
  206. $encoder = null;
  207. switch ($extension) {
  208. case 'jpeg':
  209. case 'jpg':
  210. $encoder = new JpegEncoder($quality);
  211. $outputExtension = 'jpg';
  212. break;
  213. case 'png':
  214. $encoder = new PngEncoder;
  215. $outputExtension = 'png';
  216. break;
  217. case 'webp':
  218. $encoder = new WebpEncoder($quality);
  219. $outputExtension = 'webp';
  220. break;
  221. case 'avif':
  222. $encoder = new JpegEncoder($quality);
  223. $outputExtension = 'jpg';
  224. break;
  225. case 'heic':
  226. $encoder = new JpegEncoder($quality);
  227. $outputExtension = 'jpg';
  228. break;
  229. default:
  230. $encoder = new JpegEncoder($quality);
  231. $outputExtension = 'jpg';
  232. }
  233. $converted = $this->setBaseName($path, $thumbnail, $outputExtension);
  234. $encoded = $encoder->encode($img);
  235. if ($localFs) {
  236. $newPath = storage_path('app/'.$converted['path']);
  237. file_put_contents($newPath, $encoded->toString());
  238. } else {
  239. Storage::disk($this->defaultDisk)->put(
  240. $converted['path'],
  241. $encoded->toString()
  242. );
  243. }
  244. if ($thumbnail == true) {
  245. $media->thumbnail_path = $converted['path'];
  246. $media->thumbnail_url = url(Storage::url($converted['path']));
  247. } else {
  248. $media->width = $img->width();
  249. $media->height = $img->height();
  250. $media->orientation = $orientation;
  251. $media->media_path = $converted['path'];
  252. $media->mime = 'image/'.$outputExtension;
  253. }
  254. $media->save();
  255. if ($thumbnail) {
  256. $this->generateBlurhash($media);
  257. }
  258. if ($media->status_id) {
  259. Cache::forget('status:transformer:media:attachments:'.$media->status_id);
  260. Cache::forget('status:thumb:'.$media->status_id);
  261. StatusService::del($media->status_id);
  262. }
  263. } catch (\Exception $e) {
  264. $media->processed_at = now();
  265. $media->save();
  266. if (config('app.dev_log')) {
  267. Log::info('MediaResizeException: '.$e->getMessage().' | Could not process media id: '.$media->id);
  268. }
  269. }
  270. }
  271. public function setBaseName($basePath, $thumbnail, $extension)
  272. {
  273. $path = explode('.', $basePath);
  274. $name = ($thumbnail == true) ? $path[0].'_thumb' : $path[0];
  275. $basePath = "{$name}.{$extension}";
  276. return ['path' => $basePath, 'png' => false];
  277. }
  278. protected function generateBlurhash($media)
  279. {
  280. try {
  281. if ($this->defaultDisk === 'local') {
  282. $thumbnailPath = storage_path('app/'.$media->thumbnail_path);
  283. $blurhash = Blurhash::generate($media, $thumbnailPath);
  284. } else {
  285. $tempFile = tempnam(sys_get_temp_dir(), 'blurhash_');
  286. $contents = Storage::disk($this->defaultDisk)->get($media->thumbnail_path);
  287. file_put_contents($tempFile, $contents);
  288. $blurhash = Blurhash::generate($media, $tempFile);
  289. unlink($tempFile);
  290. }
  291. if ($blurhash) {
  292. $media->blurhash = $blurhash;
  293. $media->save();
  294. }
  295. } catch (\Exception $e) {
  296. if (config('app.dev_log')) {
  297. Log::info('Blurhash generation failed: '.$e->getMessage());
  298. }
  299. }
  300. }
  301. }