ImageEncoder.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. namespace Plugin\ImageEncoder;
  20. use App\Core\Event;
  21. use App\Core\GSFile;
  22. use function App\Core\I18n\_m;
  23. use App\Core\Log;
  24. use App\Core\Modules\Plugin;
  25. use App\Util\Common;
  26. use App\Util\Exception\ClientException;
  27. use App\Util\Exception\ServerException;
  28. use App\Util\Exception\TemporaryFileException;
  29. use App\Util\Formatting;
  30. use App\Util\TemporaryFile;
  31. use Exception;
  32. use Jcupitt\Vips;
  33. use SplFileInfo;
  34. /**
  35. * Create thumbnails and validate image attachments
  36. *
  37. * @package GNUsocial
  38. * @category Attachment
  39. *
  40. * @author Diogo Peralta Cordeiro <mail@diogo.site>
  41. * @author Hugo Sales <hugo@hsal.es>
  42. * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
  43. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  44. */
  45. class ImageEncoder extends Plugin
  46. {
  47. public function version(): string
  48. {
  49. return '3.0.0';
  50. }
  51. public static function shouldHandle(string $mimetype): bool
  52. {
  53. return GSFile::mimetypeMajor($mimetype) === 'image';
  54. }
  55. public function onFileMetaAvailable(array &$event_map, string $mimetype): bool
  56. {
  57. if (!self::shouldHandle($mimetype)) {
  58. return Event::next;
  59. }
  60. $event_map['image'][] = [$this, 'fileMeta'];
  61. return Event::next;
  62. }
  63. public function onFileSanitizerAvailable(array &$event_map, string $mimetype): bool
  64. {
  65. if (!self::shouldHandle($mimetype)) {
  66. return Event::next;
  67. }
  68. $event_map['image'][] = [$this, 'fileSanitize'];
  69. return Event::next;
  70. }
  71. public function onFileResizerAvailable(array &$event_map, string $mimetype): bool
  72. {
  73. if (!self::shouldHandle($mimetype)) {
  74. return Event::next;
  75. }
  76. $event_map['image'][] = [$this, 'resizeImagePath'];
  77. return Event::next;
  78. }
  79. public function fileMeta(SplFileInfo &$file, ?string &$mimetype, ?int &$width, ?int &$height): bool
  80. {
  81. $old_limit = ini_set('memory_limit', Common::config('attachments', 'memory_limit'));
  82. try {
  83. $original_mimetype = $mimetype;
  84. if (GSFile::mimetypeMajor($original_mimetype) !== 'image') {
  85. // Nothing concerning us
  86. return false;
  87. }
  88. try {
  89. $image = Vips\Image::newFromFile($file->getRealPath(), ['access' => 'sequential']);
  90. } catch (Vips\Exception $e) {
  91. Log::debug("ImageEncoder's Vips couldn't handle the image file, failed with {$e->getMessage()}.", [$e]);
  92. return false;
  93. }
  94. $width = $image->width;
  95. $height = $image->height;
  96. } finally {
  97. ini_set('memory_limit', $old_limit); // Restore the old memory limit
  98. }
  99. // Only one plugin can handle meta
  100. return true;
  101. }
  102. /**
  103. * Re-encodes the image ensuring it is valid.
  104. * Also ensures that the image is not greater than the max width and height configured.
  105. *
  106. * @param null|string $mimetype in/out
  107. * @param null|int $width out
  108. * @param null|int $height out
  109. *
  110. * @throws ClientException When vips doesn't understand the given mimetype
  111. * @throws ServerException
  112. * @throws TemporaryFileException
  113. * @throws Vips\Exception
  114. *
  115. * @return bool true if sanitized
  116. */
  117. public function fileSanitize(SplFileInfo &$file, ?string &$mimetype, ?int &$width, ?int &$height): bool
  118. {
  119. $old_limit = ini_set('memory_limit', Common::config('attachments', 'memory_limit'));
  120. try {
  121. $original_mimetype = $mimetype;
  122. if (GSFile::mimetypeMajor($original_mimetype) !== 'image') {
  123. // Nothing concerning us
  124. return false;
  125. }
  126. // Try to maintain original mimetype extension, otherwise default to preferred.
  127. $extension = '.' . Common::config('thumbnail', 'extension');
  128. $extension = GSFile::ensureFilenameWithProperExtension(
  129. title: $file->getFilename(),
  130. mimetype: $original_mimetype,
  131. ext: $extension,
  132. force: false,
  133. ) ?? $extension;
  134. // TemporaryFile handles deleting the file if some error occurs
  135. // IMPORTANT: We have to specify the extension for the temporary file
  136. // in order to have a format conversion
  137. $temp = new TemporaryFile(['prefix' => 'image', 'suffix' => $extension]);
  138. try {
  139. $image = Vips\Image::newFromFile($file->getRealPath(), ['access' => 'sequential']);
  140. } catch (Vips\Exception $e) {
  141. Log::debug("ImageEncoder's Vips couldn't handle the image file, failed with {$e->getMessage()}.", [$e]);
  142. return false;
  143. }
  144. $width = $image->width;
  145. $height = $image->height;
  146. $image = $image->crop(
  147. left: 0,
  148. top: 0,
  149. width: $width,
  150. height: $height,
  151. );
  152. $image->writeToFile($temp->getRealPath());
  153. // Replace original file with the sanitized one
  154. $temp->commit($file->getRealPath());
  155. } finally {
  156. ini_set('memory_limit', $old_limit); // Restore the old memory limit
  157. }
  158. // Only one plugin can handle sanitization
  159. return true;
  160. }
  161. /**
  162. * Generates the view for attachments of type Image
  163. */
  164. public function onViewAttachment(array $vars, array &$res): bool
  165. {
  166. if (!self::shouldHandle($vars['attachment']->getMimetype())) {
  167. return Event::next;
  168. }
  169. if (\is_null($thumbnail = $vars['attachment']->getThumbnail())) {
  170. return Event::next;
  171. }
  172. $res[] = Formatting::twigRenderFile(
  173. 'imageEncoder/imageEncoderView.html.twig',
  174. [
  175. 'attachment' => $vars['attachment'],
  176. 'note' => $vars['note'],
  177. 'title' => $vars['title'],
  178. 'thumbnail' => $thumbnail,
  179. ],
  180. );
  181. return Event::stop;
  182. }
  183. /**
  184. * Resizes an image. It will encode the image in the
  185. * preferred thumbnail extension. This only applies henceforward,
  186. * not retroactively
  187. *
  188. * Increases the 'memory_limit' to the one in the 'attachments' section in the config, to
  189. * enable the handling of bigger images, which can cause a peak of memory consumption, while
  190. * encoding
  191. *
  192. * @throws TemporaryFileException
  193. * @throws Vips\Exception
  194. */
  195. public function resizeImagePath(string $source, ?TemporaryFile &$destination, int &$width, int &$height, bool $smart_crop, ?string &$mimetype): bool
  196. {
  197. $old_limit = ini_set('memory_limit', Common::config('attachments', 'memory_limit'));
  198. try {
  199. try {
  200. if (!$smart_crop) {
  201. $image = Vips\Image::thumbnail($source, $width, ['height' => $height]);
  202. } else {
  203. $image = Vips\Image::newFromFile($source, ['access' => 'sequential']);
  204. $image = $image->smartcrop($width, $height, [Vips\Interesting::ATTENTION]);
  205. }
  206. } catch (Exception $e) {
  207. Log::error(__METHOD__ . ' encountered exception: ' . \get_class($e));
  208. // TRANS: Exception thrown when trying to resize an unknown file type.
  209. throw new Exception(_m('Unknown file type'));
  210. }
  211. if (\is_null($destination)) {
  212. // IMPORTANT: We have to specify the extension for the temporary file
  213. // in order to have a format conversion
  214. $ext = '.' . Common::config('thumbnail', 'extension');
  215. $destination = new TemporaryFile(['prefix' => 'gs-thumbnail', 'suffix' => $ext]);
  216. } elseif ($source === $destination->getRealPath()) {
  217. @unlink($destination->getRealPath());
  218. }
  219. $mimetype = Common::config('thumbnail', 'mimetype');
  220. $width = $image->width;
  221. $height = $image->height;
  222. $image->writeToFile($destination->getRealPath());
  223. unset($image);
  224. } finally {
  225. ini_set('memory_limit', $old_limit); // Restore the old memory limit
  226. }
  227. return true;
  228. }
  229. /**
  230. * Event raised when GNU social polls the plugin for information about it.
  231. * Adds this plugin's version information to $versions array
  232. *
  233. * @param array $versions inherited from parent
  234. *
  235. * @return bool true hook value
  236. */
  237. public function onPluginVersion(array &$versions): bool
  238. {
  239. $versions[] = [
  240. 'name' => 'ImageEncoder',
  241. 'version' => $this->version(),
  242. 'author' => 'Hugo Sales, Diogo Peralta Cordeiro',
  243. 'homepage' => GNUSOCIAL_PROJECT_URL,
  244. 'description', // TRANS: Plugin description. => _m('Use VIPS for some additional image support.'),
  245. ];
  246. return Event::next;
  247. }
  248. }