PageRenderTime 42ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/includes/media/SVG.php

https://gitlab.com/qiusct/mediawiki-i
PHP | 518 lines | 327 code | 59 blank | 132 comment | 62 complexity | d5acda3722b62f9e0ad1980fd825586a MD5 | raw file
Possible License(s): Apache-2.0, MIT, GPL-2.0
  1. <?php
  2. /**
  3. * Handler for SVG images.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program 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 General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Media
  22. */
  23. /**
  24. * Handler for SVG images.
  25. *
  26. * @ingroup Media
  27. */
  28. class SvgHandler extends ImageHandler {
  29. const SVG_METADATA_VERSION = 2;
  30. /** @var array A list of metadata tags that can be converted
  31. * to the commonly used exif tags. This allows messages
  32. * to be reused, and consistent tag names for {{#formatmetadata:..}}
  33. */
  34. private static $metaConversion = array(
  35. 'originalwidth' => 'ImageWidth',
  36. 'originalheight' => 'ImageLength',
  37. 'description' => 'ImageDescription',
  38. 'title' => 'ObjectName',
  39. );
  40. function isEnabled() {
  41. global $wgSVGConverters, $wgSVGConverter;
  42. if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) {
  43. wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering.\n" );
  44. return false;
  45. } else {
  46. return true;
  47. }
  48. }
  49. function mustRender( $file ) {
  50. return true;
  51. }
  52. function isVectorized( $file ) {
  53. return true;
  54. }
  55. /**
  56. * @param File $file
  57. * @return bool
  58. */
  59. function isAnimatedImage( $file ) {
  60. # @todo Detect animated SVGs
  61. $metadata = $file->getMetadata();
  62. if ( $metadata ) {
  63. $metadata = $this->unpackMetadata( $metadata );
  64. if ( isset( $metadata['animated'] ) ) {
  65. return $metadata['animated'];
  66. }
  67. }
  68. return false;
  69. }
  70. /**
  71. * Which languages (systemLanguage attribute) is supported.
  72. *
  73. * @note This list is not guaranteed to be exhaustive.
  74. * To avoid OOM errors, we only look at first bit of a file.
  75. * Thus all languages on this list are present in the file,
  76. * but its possible for the file to have a language not on
  77. * this list.
  78. *
  79. * @param File $file
  80. * @return array Array of language codes, or empty if no language switching supported.
  81. */
  82. public function getAvailableLanguages( File $file ) {
  83. $metadata = $file->getMetadata();
  84. $langList = array();
  85. if ( $metadata ) {
  86. $metadata = $this->unpackMetadata( $metadata );
  87. if ( isset( $metadata['translations'] ) ) {
  88. foreach ( $metadata['translations'] as $lang => $langType ) {
  89. if ( $langType === SvgReader::LANG_FULL_MATCH ) {
  90. $langList[] = $lang;
  91. }
  92. }
  93. }
  94. }
  95. return $langList;
  96. }
  97. /**
  98. * What language to render file in if none selected.
  99. *
  100. * @return string Language code.
  101. */
  102. public function getDefaultRenderLanguage( File $file ) {
  103. return 'en';
  104. }
  105. /**
  106. * We do not support making animated svg thumbnails
  107. */
  108. function canAnimateThumb( $file ) {
  109. return false;
  110. }
  111. /**
  112. * @param File $image
  113. * @param array $params
  114. * @return bool
  115. */
  116. function normaliseParams( $image, &$params ) {
  117. global $wgSVGMaxSize;
  118. if ( !parent::normaliseParams( $image, $params ) ) {
  119. return false;
  120. }
  121. # Don't make an image bigger than wgMaxSVGSize on the smaller side
  122. if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
  123. if ( $params['physicalWidth'] > $wgSVGMaxSize ) {
  124. $srcWidth = $image->getWidth( $params['page'] );
  125. $srcHeight = $image->getHeight( $params['page'] );
  126. $params['physicalWidth'] = $wgSVGMaxSize;
  127. $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize );
  128. }
  129. } else {
  130. if ( $params['physicalHeight'] > $wgSVGMaxSize ) {
  131. $srcWidth = $image->getWidth( $params['page'] );
  132. $srcHeight = $image->getHeight( $params['page'] );
  133. $params['physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $wgSVGMaxSize );
  134. $params['physicalHeight'] = $wgSVGMaxSize;
  135. }
  136. }
  137. return true;
  138. }
  139. /**
  140. * @param File $image
  141. * @param string $dstPath
  142. * @param string $dstUrl
  143. * @param array $params
  144. * @param int $flags
  145. * @return bool|MediaTransformError|ThumbnailImage|TransformParameterError
  146. */
  147. function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
  148. if ( !$this->normaliseParams( $image, $params ) ) {
  149. return new TransformParameterError( $params );
  150. }
  151. $clientWidth = $params['width'];
  152. $clientHeight = $params['height'];
  153. $physicalWidth = $params['physicalWidth'];
  154. $physicalHeight = $params['physicalHeight'];
  155. $lang = isset( $params['lang'] ) ? $params['lang'] : $this->getDefaultRenderLanguage( $image );
  156. if ( $flags & self::TRANSFORM_LATER ) {
  157. return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
  158. }
  159. $metadata = $this->unpackMetadata( $image->getMetadata() );
  160. if ( isset( $metadata['error'] ) ) { // sanity check
  161. $err = wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
  162. return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
  163. }
  164. if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
  165. return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
  166. wfMessage( 'thumbnail_dest_directory' )->text() );
  167. }
  168. $srcPath = $image->getLocalRefPath();
  169. $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
  170. if ( $status === true ) {
  171. return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
  172. } else {
  173. return $status; // MediaTransformError
  174. }
  175. }
  176. /**
  177. * Transform an SVG file to PNG
  178. * This function can be called outside of thumbnail contexts
  179. * @param string $srcPath
  180. * @param string $dstPath
  181. * @param string $width
  182. * @param string $height
  183. * @param bool|string $lang Language code of the language to render the SVG in
  184. * @throws MWException
  185. * @return bool|MediaTransformError
  186. */
  187. public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
  188. global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
  189. $err = false;
  190. $retval = '';
  191. if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
  192. if ( is_array( $wgSVGConverters[$wgSVGConverter] ) ) {
  193. // This is a PHP callable
  194. $func = $wgSVGConverters[$wgSVGConverter][0];
  195. $args = array_merge( array( $srcPath, $dstPath, $width, $height, $lang ),
  196. array_slice( $wgSVGConverters[$wgSVGConverter], 1 ) );
  197. if ( !is_callable( $func ) ) {
  198. throw new MWException( "$func is not callable" );
  199. }
  200. $err = call_user_func_array( $func, $args );
  201. $retval = (bool)$err;
  202. } else {
  203. // External command
  204. $cmd = str_replace(
  205. array( '$path/', '$width', '$height', '$input', '$output' ),
  206. array( $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "",
  207. intval( $width ),
  208. intval( $height ),
  209. wfEscapeShellArg( $srcPath ),
  210. wfEscapeShellArg( $dstPath ) ),
  211. $wgSVGConverters[$wgSVGConverter]
  212. );
  213. $env = array();
  214. if ( $lang !== false ) {
  215. $env['LANG'] = $lang;
  216. }
  217. wfProfileIn( 'rsvg' );
  218. wfDebug( __METHOD__ . ": $cmd\n" );
  219. $err = wfShellExecWithStderr( $cmd, $retval, $env );
  220. wfProfileOut( 'rsvg' );
  221. }
  222. }
  223. $removed = $this->removeBadFile( $dstPath, $retval );
  224. if ( $retval != 0 || $removed ) {
  225. $this->logErrorForExternalProcess( $retval, $err, $cmd );
  226. return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
  227. }
  228. return true;
  229. }
  230. public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
  231. $im = new Imagick( $srcPath );
  232. $im->setImageFormat( 'png' );
  233. $im->setBackgroundColor( 'transparent' );
  234. $im->setImageDepth( 8 );
  235. if ( !$im->thumbnailImage( intval( $width ), intval( $height ), /* fit */ false ) ) {
  236. return 'Could not resize image';
  237. }
  238. if ( !$im->writeImage( $dstPath ) ) {
  239. return "Could not write to $dstPath";
  240. }
  241. }
  242. /**
  243. * @param File $file
  244. * @param string $path Unused
  245. * @param bool|array $metadata
  246. * @return array
  247. */
  248. function getImageSize( $file, $path, $metadata = false ) {
  249. if ( $metadata === false ) {
  250. $metadata = $file->getMetaData();
  251. }
  252. $metadata = $this->unpackMetaData( $metadata );
  253. if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) {
  254. return array( $metadata['width'], $metadata['height'], 'SVG',
  255. "width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" );
  256. } else { // error
  257. return array( 0, 0, 'SVG', "width=\"0\" height=\"0\"" );
  258. }
  259. }
  260. function getThumbType( $ext, $mime, $params = null ) {
  261. return array( 'png', 'image/png' );
  262. }
  263. /**
  264. * Subtitle for the image. Different from the base
  265. * class so it can be denoted that SVG's have
  266. * a "nominal" resolution, and not a fixed one,
  267. * as well as so animation can be denoted.
  268. *
  269. * @param File $file
  270. * @return string
  271. */
  272. function getLongDesc( $file ) {
  273. global $wgLang;
  274. $metadata = $this->unpackMetadata( $file->getMetadata() );
  275. if ( isset( $metadata['error'] ) ) {
  276. return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
  277. }
  278. $size = $wgLang->formatSize( $file->getSize() );
  279. if ( $this->isAnimatedImage( $file ) ) {
  280. $msg = wfMessage( 'svg-long-desc-animated' );
  281. } else {
  282. $msg = wfMessage( 'svg-long-desc' );
  283. }
  284. $msg->numParams( $file->getWidth(), $file->getHeight() )->params( $size );
  285. return $msg->parse();
  286. }
  287. /**
  288. * @param File $file
  289. * @param string $filename
  290. * @return string Serialised metadata
  291. */
  292. function getMetadata( $file, $filename ) {
  293. $metadata = array( 'version' => self::SVG_METADATA_VERSION );
  294. try {
  295. $metadata += SVGMetadataExtractor::getMetadata( $filename );
  296. } catch ( MWException $e ) { // @todo SVG specific exceptions
  297. // File not found, broken, etc.
  298. $metadata['error'] = array(
  299. 'message' => $e->getMessage(),
  300. 'code' => $e->getCode()
  301. );
  302. wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
  303. }
  304. return serialize( $metadata );
  305. }
  306. function unpackMetadata( $metadata ) {
  307. wfSuppressWarnings();
  308. $unser = unserialize( $metadata );
  309. wfRestoreWarnings();
  310. if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
  311. return $unser;
  312. } else {
  313. return false;
  314. }
  315. }
  316. function getMetadataType( $image ) {
  317. return 'parsed-svg';
  318. }
  319. function isMetadataValid( $image, $metadata ) {
  320. $meta = $this->unpackMetadata( $metadata );
  321. if ( $meta === false ) {
  322. return self::METADATA_BAD;
  323. }
  324. if ( !isset( $meta['originalWidth'] ) ) {
  325. // Old but compatible
  326. return self::METADATA_COMPATIBLE;
  327. }
  328. return self::METADATA_GOOD;
  329. }
  330. protected function visibleMetadataFields() {
  331. $fields = array( 'objectname', 'imagedescription' );
  332. return $fields;
  333. }
  334. /**
  335. * @param File $file
  336. * @return array|bool
  337. */
  338. function formatMetadata( $file ) {
  339. $result = array(
  340. 'visible' => array(),
  341. 'collapsed' => array()
  342. );
  343. $metadata = $file->getMetadata();
  344. if ( !$metadata ) {
  345. return false;
  346. }
  347. $metadata = $this->unpackMetadata( $metadata );
  348. if ( !$metadata || isset( $metadata['error'] ) ) {
  349. return false;
  350. }
  351. /* @todo Add a formatter
  352. $format = new FormatSVG( $metadata );
  353. $formatted = $format->getFormattedData();
  354. */
  355. // Sort fields into visible and collapsed
  356. $visibleFields = $this->visibleMetadataFields();
  357. $showMeta = false;
  358. foreach ( $metadata as $name => $value ) {
  359. $tag = strtolower( $name );
  360. if ( isset( self::$metaConversion[$tag] ) ) {
  361. $tag = strtolower( self::$metaConversion[$tag] );
  362. } else {
  363. // Do not output other metadata not in list
  364. continue;
  365. }
  366. $showMeta = true;
  367. self::addMeta( $result,
  368. in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
  369. 'exif',
  370. $tag,
  371. $value
  372. );
  373. }
  374. return $showMeta ? $result : false;
  375. }
  376. /**
  377. * @param string $name Parameter name
  378. * @param mixed $value Parameter value
  379. * @return bool Validity
  380. */
  381. function validateParam( $name, $value ) {
  382. if ( in_array( $name, array( 'width', 'height' ) ) ) {
  383. // Reject negative heights, widths
  384. return ( $value > 0 );
  385. } elseif ( $name == 'lang' ) {
  386. // Validate $code
  387. if ( $value === '' || !Language::isValidBuiltinCode( $value ) ) {
  388. wfDebug( "Invalid user language code\n" );
  389. return false;
  390. }
  391. return true;
  392. }
  393. // Only lang, width and height are acceptable keys
  394. return false;
  395. }
  396. /**
  397. * @param array $params name=>value pairs of parameters
  398. * @return string Filename to use
  399. */
  400. function makeParamString( $params ) {
  401. $lang = '';
  402. if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
  403. $params['lang'] = mb_strtolower( $params['lang'] );
  404. $lang = "lang{$params['lang']}-";
  405. }
  406. if ( !isset( $params['width'] ) ) {
  407. return false;
  408. }
  409. return "$lang{$params['width']}px";
  410. }
  411. function parseParamString( $str ) {
  412. $m = false;
  413. if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
  414. return array( 'width' => array_pop( $m ), 'lang' => $m[1] );
  415. } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
  416. return array( 'width' => $m[1], 'lang' => 'en' );
  417. } else {
  418. return false;
  419. }
  420. }
  421. function getParamMap() {
  422. return array( 'img_lang' => 'lang', 'img_width' => 'width' );
  423. }
  424. /**
  425. * @param array $params
  426. * @return array
  427. */
  428. function getScriptParams( $params ) {
  429. $scriptParams = array( 'width' => $params['width'] );
  430. if ( isset( $params['lang'] ) ) {
  431. $scriptParams['lang'] = $params['lang'];
  432. }
  433. return $scriptParams;
  434. }
  435. public function getCommonMetaArray( File $file ) {
  436. $metadata = $file->getMetadata();
  437. if ( !$metadata ) {
  438. return array();
  439. }
  440. $metadata = $this->unpackMetadata( $metadata );
  441. if ( !$metadata || isset( $metadata['error'] ) ) {
  442. return array();
  443. }
  444. $stdMetadata = array();
  445. foreach ( $metadata as $name => $value ) {
  446. $tag = strtolower( $name );
  447. if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
  448. // Skip these. In the exif metadata stuff, it is assumed these
  449. // are measured in px, which is not the case here.
  450. continue;
  451. }
  452. if ( isset( self::$metaConversion[$tag] ) ) {
  453. $tag = self::$metaConversion[$tag];
  454. $stdMetadata[$tag] = $value;
  455. }
  456. }
  457. return $stdMetadata;
  458. }
  459. }