ImageEncoder.php 8.5 KB

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