ImageEncoder.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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 Plugin\ImageEncoder\Exception\UnsupportedFileTypeException;
  34. use SplFileInfo;
  35. /**
  36. * Create thumbnails and validate image attachments
  37. *
  38. * @package GNUsocial
  39. * @category Attachment
  40. *
  41. * @author Diogo Peralta Cordeiro <mail@diogo.site>
  42. * @author Hugo Sales <hugo@hsal.es>
  43. * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
  44. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  45. */
  46. class ImageEncoder extends Plugin
  47. {
  48. public function version(): string
  49. {
  50. return '3.0.0';
  51. }
  52. public static function shouldHandle (string $mimetype): bool {
  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}.");
  92. throw new UnsupportedFileTypeException(_m("Unsupported image file with {$mimetype}.", previous: $e));
  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}.");
  142. throw new UnsupportedFileTypeException(_m("Unsupported image file with {$mimetype}.", previous: $e));
  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. $res[] = Formatting::twigRenderFile(
  170. 'imageEncoder/imageEncoderView.html.twig',
  171. [
  172. 'attachment' => $vars['attachment'],
  173. 'note' => $vars['note'],
  174. ],
  175. );
  176. return Event::stop;
  177. }
  178. /**
  179. * Resizes an image. It will encode the image in the
  180. * preferred thumbnail extension. This only applies henceforward,
  181. * not retroactively
  182. *
  183. * Increases the 'memory_limit' to the one in the 'attachments' section in the config, to
  184. * enable the handling of bigger images, which can cause a peak of memory consumption, while
  185. * encoding
  186. *
  187. * @throws TemporaryFileException
  188. * @throws Vips\Exception
  189. */
  190. public function resizeImagePath(string $source, ?TemporaryFile &$destination, int &$width, int &$height, bool $smart_crop, ?string &$mimetype): bool
  191. {
  192. $old_limit = ini_set('memory_limit', Common::config('attachments', 'memory_limit'));
  193. try {
  194. try {
  195. if (!$smart_crop) {
  196. $image = Vips\Image::thumbnail($source, $width, ['height' => $height]);
  197. } else {
  198. $image = Vips\Image::newFromFile($source, ['access' => 'sequential']);
  199. $image = $image->smartcrop($width, $height, [Vips\Interesting::ATTENTION]);
  200. }
  201. } catch (Exception $e) {
  202. Log::error(__METHOD__ . ' encountered exception: ' . \get_class($e));
  203. // TRANS: Exception thrown when trying to resize an unknown file type.
  204. throw new Exception(_m('Unknown file type'));
  205. }
  206. if (\is_null($destination)) {
  207. // IMPORTANT: We have to specify the extension for the temporary file
  208. // in order to have a format conversion
  209. $ext = '.' . Common::config('thumbnail', 'extension');
  210. $destination = new TemporaryFile(['prefix' => 'gs-thumbnail', 'suffix' => $ext]);
  211. } elseif ($source === $destination->getRealPath()) {
  212. @unlink($destination->getRealPath());
  213. }
  214. $mimetype = Common::config('thumbnail', 'mimetype');
  215. $width = $image->width;
  216. $height = $image->height;
  217. $image->writeToFile($destination->getRealPath());
  218. unset($image);
  219. } finally {
  220. ini_set('memory_limit', $old_limit); // Restore the old memory limit
  221. }
  222. return true;
  223. }
  224. /**
  225. * Event raised when GNU social polls the plugin for information about it.
  226. * Adds this plugin's version information to $versions array
  227. *
  228. * @param array $versions inherited from parent
  229. *
  230. * @return bool true hook value
  231. */
  232. public function onPluginVersion(array &$versions): bool
  233. {
  234. $versions[] = [
  235. 'name' => 'ImageEncoder',
  236. 'version' => $this->version(),
  237. 'author' => 'Hugo Sales, Diogo Peralta Cordeiro',
  238. 'homepage' => GNUSOCIAL_PROJECT_URL,
  239. 'description', // TRANS: Plugin description. => _m('Use VIPS for some additional image support.'),
  240. ];
  241. return Event::next;
  242. }
  243. }