]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/media/SVG.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / media / SVG.php
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 use Wikimedia\ScopedCallback;
24
25 /**
26  * Handler for SVG images.
27  *
28  * @ingroup Media
29  */
30 class SvgHandler extends ImageHandler {
31         const SVG_METADATA_VERSION = 2;
32
33         /** @var array A list of metadata tags that can be converted
34          *  to the commonly used exif tags. This allows messages
35          *  to be reused, and consistent tag names for {{#formatmetadata:..}}
36          */
37         private static $metaConversion = [
38                 'originalwidth' => 'ImageWidth',
39                 'originalheight' => 'ImageLength',
40                 'description' => 'ImageDescription',
41                 'title' => 'ObjectName',
42         ];
43
44         function isEnabled() {
45                 global $wgSVGConverters, $wgSVGConverter;
46                 if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) {
47                         wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering.\n" );
48
49                         return false;
50                 } else {
51                         return true;
52                 }
53         }
54
55         public function mustRender( $file ) {
56                 return true;
57         }
58
59         function isVectorized( $file ) {
60                 return true;
61         }
62
63         /**
64          * @param File $file
65          * @return bool
66          */
67         function isAnimatedImage( $file ) {
68                 # @todo Detect animated SVGs
69                 $metadata = $file->getMetadata();
70                 if ( $metadata ) {
71                         $metadata = $this->unpackMetadata( $metadata );
72                         if ( isset( $metadata['animated'] ) ) {
73                                 return $metadata['animated'];
74                         }
75                 }
76
77                 return false;
78         }
79
80         /**
81          * Which languages (systemLanguage attribute) is supported.
82          *
83          * @note This list is not guaranteed to be exhaustive.
84          * To avoid OOM errors, we only look at first bit of a file.
85          * Thus all languages on this list are present in the file,
86          * but its possible for the file to have a language not on
87          * this list.
88          *
89          * @param File $file
90          * @return array Array of language codes, or empty if no language switching supported.
91          */
92         public function getAvailableLanguages( File $file ) {
93                 $metadata = $file->getMetadata();
94                 $langList = [];
95                 if ( $metadata ) {
96                         $metadata = $this->unpackMetadata( $metadata );
97                         if ( isset( $metadata['translations'] ) ) {
98                                 foreach ( $metadata['translations'] as $lang => $langType ) {
99                                         if ( $langType === SVGReader::LANG_FULL_MATCH ) {
100                                                 $langList[] = $lang;
101                                         }
102                                 }
103                         }
104                 }
105                 return $langList;
106         }
107
108         /**
109          * What language to render file in if none selected.
110          *
111          * @param File $file
112          * @return string Language code.
113          */
114         public function getDefaultRenderLanguage( File $file ) {
115                 return 'en';
116         }
117
118         /**
119          * We do not support making animated svg thumbnails
120          * @param File $file
121          * @return bool
122          */
123         function canAnimateThumbnail( $file ) {
124                 return false;
125         }
126
127         /**
128          * @param File $image
129          * @param array &$params
130          * @return bool
131          */
132         function normaliseParams( $image, &$params ) {
133                 global $wgSVGMaxSize;
134                 if ( !parent::normaliseParams( $image, $params ) ) {
135                         return false;
136                 }
137                 # Don't make an image bigger than wgMaxSVGSize on the smaller side
138                 if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
139                         if ( $params['physicalWidth'] > $wgSVGMaxSize ) {
140                                 $srcWidth = $image->getWidth( $params['page'] );
141                                 $srcHeight = $image->getHeight( $params['page'] );
142                                 $params['physicalWidth'] = $wgSVGMaxSize;
143                                 $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize );
144                         }
145                 } else {
146                         if ( $params['physicalHeight'] > $wgSVGMaxSize ) {
147                                 $srcWidth = $image->getWidth( $params['page'] );
148                                 $srcHeight = $image->getHeight( $params['page'] );
149                                 $params['physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $wgSVGMaxSize );
150                                 $params['physicalHeight'] = $wgSVGMaxSize;
151                         }
152                 }
153
154                 return true;
155         }
156
157         /**
158          * @param File $image
159          * @param string $dstPath
160          * @param string $dstUrl
161          * @param array $params
162          * @param int $flags
163          * @return bool|MediaTransformError|ThumbnailImage|TransformParameterError
164          */
165         function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
166                 if ( !$this->normaliseParams( $image, $params ) ) {
167                         return new TransformParameterError( $params );
168                 }
169                 $clientWidth = $params['width'];
170                 $clientHeight = $params['height'];
171                 $physicalWidth = $params['physicalWidth'];
172                 $physicalHeight = $params['physicalHeight'];
173                 $lang = isset( $params['lang'] ) ? $params['lang'] : $this->getDefaultRenderLanguage( $image );
174
175                 if ( $flags & self::TRANSFORM_LATER ) {
176                         return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
177                 }
178
179                 $metadata = $this->unpackMetadata( $image->getMetadata() );
180                 if ( isset( $metadata['error'] ) ) { // sanity check
181                         $err = wfMessage( 'svg-long-error', $metadata['error']['message'] );
182
183                         return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
184                 }
185
186                 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
187                         return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
188                                 wfMessage( 'thumbnail_dest_directory' ) );
189                 }
190
191                 $srcPath = $image->getLocalRefPath();
192                 if ( $srcPath === false ) { // Failed to get local copy
193                         wfDebugLog( 'thumbnail',
194                                 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
195                                         wfHostname(), $image->getName() ) );
196
197                         return new MediaTransformError( 'thumbnail_error',
198                                 $params['width'], $params['height'],
199                                 wfMessage( 'filemissing' )
200                         );
201                 }
202
203                 // Make a temp dir with a symlink to the local copy in it.
204                 // This plays well with rsvg-convert policy for external entities.
205                 // https://git.gnome.org/browse/librsvg/commit/?id=f01aded72c38f0e18bc7ff67dee800e380251c8e
206                 $tmpDir = wfTempDir() . '/svg_' . wfRandomString( 24 );
207                 $lnPath = "$tmpDir/" . basename( $srcPath );
208                 $ok = mkdir( $tmpDir, 0771 );
209                 if ( !$ok ) {
210                         wfDebugLog( 'thumbnail',
211                                 sprintf( 'Thumbnail failed on %s: could not create temporary directory %s',
212                                         wfHostname(), $tmpDir ) );
213                         return new MediaTransformError( 'thumbnail_error',
214                                 $params['width'], $params['height'],
215                                 wfMessage( 'thumbnail-temp-create' )->text()
216                         );
217                 }
218                 $ok = symlink( $srcPath, $lnPath );
219                 /** @noinspection PhpUnusedLocalVariableInspection */
220                 $cleaner = new ScopedCallback( function () use ( $tmpDir, $lnPath ) {
221                         MediaWiki\suppressWarnings();
222                         unlink( $lnPath );
223                         rmdir( $tmpDir );
224                         MediaWiki\restoreWarnings();
225                 } );
226                 if ( !$ok ) {
227                         wfDebugLog( 'thumbnail',
228                                 sprintf( 'Thumbnail failed on %s: could not link %s to %s',
229                                         wfHostname(), $lnPath, $srcPath ) );
230                         return new MediaTransformError( 'thumbnail_error',
231                                 $params['width'], $params['height'],
232                                 wfMessage( 'thumbnail-temp-create' )
233                         );
234                 }
235
236                 $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
237                 if ( $status === true ) {
238                         return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
239                 } else {
240                         return $status; // MediaTransformError
241                 }
242         }
243
244         /**
245          * Transform an SVG file to PNG
246          * This function can be called outside of thumbnail contexts
247          * @param string $srcPath
248          * @param string $dstPath
249          * @param string $width
250          * @param string $height
251          * @param bool|string $lang Language code of the language to render the SVG in
252          * @throws MWException
253          * @return bool|MediaTransformError
254          */
255         public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
256                 global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
257                 $err = false;
258                 $retval = '';
259                 if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
260                         if ( is_array( $wgSVGConverters[$wgSVGConverter] ) ) {
261                                 // This is a PHP callable
262                                 $func = $wgSVGConverters[$wgSVGConverter][0];
263                                 $args = array_merge( [ $srcPath, $dstPath, $width, $height, $lang ],
264                                         array_slice( $wgSVGConverters[$wgSVGConverter], 1 ) );
265                                 if ( !is_callable( $func ) ) {
266                                         throw new MWException( "$func is not callable" );
267                                 }
268                                 $err = call_user_func_array( $func, $args );
269                                 $retval = (bool)$err;
270                         } else {
271                                 // External command
272                                 $cmd = str_replace(
273                                         [ '$path/', '$width', '$height', '$input', '$output' ],
274                                         [ $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "",
275                                                 intval( $width ),
276                                                 intval( $height ),
277                                                 wfEscapeShellArg( $srcPath ),
278                                                 wfEscapeShellArg( $dstPath ) ],
279                                         $wgSVGConverters[$wgSVGConverter]
280                                 );
281
282                                 $env = [];
283                                 if ( $lang !== false ) {
284                                         $env['LANG'] = $lang;
285                                 }
286
287                                 wfDebug( __METHOD__ . ": $cmd\n" );
288                                 $err = wfShellExecWithStderr( $cmd, $retval, $env );
289                         }
290                 }
291                 $removed = $this->removeBadFile( $dstPath, $retval );
292                 if ( $retval != 0 || $removed ) {
293                         $this->logErrorForExternalProcess( $retval, $err, $cmd );
294                         return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
295                 }
296
297                 return true;
298         }
299
300         public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
301                 $im = new Imagick( $srcPath );
302                 $im->setImageFormat( 'png' );
303                 $im->setBackgroundColor( 'transparent' );
304                 $im->setImageDepth( 8 );
305
306                 if ( !$im->thumbnailImage( intval( $width ), intval( $height ), /* fit */ false ) ) {
307                         return 'Could not resize image';
308                 }
309                 if ( !$im->writeImage( $dstPath ) ) {
310                         return "Could not write to $dstPath";
311                 }
312         }
313
314         /**
315          * @param File|FSFile $file
316          * @param string $path Unused
317          * @param bool|array $metadata
318          * @return array
319          */
320         function getImageSize( $file, $path, $metadata = false ) {
321                 if ( $metadata === false && $file instanceof File ) {
322                         $metadata = $file->getMetadata();
323                 }
324                 $metadata = $this->unpackMetadata( $metadata );
325
326                 if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) {
327                         return [ $metadata['width'], $metadata['height'], 'SVG',
328                                 "width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" ];
329                 } else { // error
330                         return [ 0, 0, 'SVG', "width=\"0\" height=\"0\"" ];
331                 }
332         }
333
334         function getThumbType( $ext, $mime, $params = null ) {
335                 return [ 'png', 'image/png' ];
336         }
337
338         /**
339          * Subtitle for the image. Different from the base
340          * class so it can be denoted that SVG's have
341          * a "nominal" resolution, and not a fixed one,
342          * as well as so animation can be denoted.
343          *
344          * @param File $file
345          * @return string
346          */
347         function getLongDesc( $file ) {
348                 global $wgLang;
349
350                 $metadata = $this->unpackMetadata( $file->getMetadata() );
351                 if ( isset( $metadata['error'] ) ) {
352                         return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
353                 }
354
355                 $size = $wgLang->formatSize( $file->getSize() );
356
357                 if ( $this->isAnimatedImage( $file ) ) {
358                         $msg = wfMessage( 'svg-long-desc-animated' );
359                 } else {
360                         $msg = wfMessage( 'svg-long-desc' );
361                 }
362
363                 $msg->numParams( $file->getWidth(), $file->getHeight() )->params( $size );
364
365                 return $msg->parse();
366         }
367
368         /**
369          * @param File|FSFile $file
370          * @param string $filename
371          * @return string Serialised metadata
372          */
373         function getMetadata( $file, $filename ) {
374                 $metadata = [ 'version' => self::SVG_METADATA_VERSION ];
375                 try {
376                         $metadata += SVGMetadataExtractor::getMetadata( $filename );
377                 } catch ( Exception $e ) { // @todo SVG specific exceptions
378                         // File not found, broken, etc.
379                         $metadata['error'] = [
380                                 'message' => $e->getMessage(),
381                                 'code' => $e->getCode()
382                         ];
383                         wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
384                 }
385
386                 return serialize( $metadata );
387         }
388
389         function unpackMetadata( $metadata ) {
390                 MediaWiki\suppressWarnings();
391                 $unser = unserialize( $metadata );
392                 MediaWiki\restoreWarnings();
393                 if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
394                         return $unser;
395                 } else {
396                         return false;
397                 }
398         }
399
400         function getMetadataType( $image ) {
401                 return 'parsed-svg';
402         }
403
404         function isMetadataValid( $image, $metadata ) {
405                 $meta = $this->unpackMetadata( $metadata );
406                 if ( $meta === false ) {
407                         return self::METADATA_BAD;
408                 }
409                 if ( !isset( $meta['originalWidth'] ) ) {
410                         // Old but compatible
411                         return self::METADATA_COMPATIBLE;
412                 }
413
414                 return self::METADATA_GOOD;
415         }
416
417         protected function visibleMetadataFields() {
418                 $fields = [ 'objectname', 'imagedescription' ];
419
420                 return $fields;
421         }
422
423         /**
424          * @param File $file
425          * @param bool|IContextSource $context Context to use (optional)
426          * @return array|bool
427          */
428         function formatMetadata( $file, $context = false ) {
429                 $result = [
430                         'visible' => [],
431                         'collapsed' => []
432                 ];
433                 $metadata = $file->getMetadata();
434                 if ( !$metadata ) {
435                         return false;
436                 }
437                 $metadata = $this->unpackMetadata( $metadata );
438                 if ( !$metadata || isset( $metadata['error'] ) ) {
439                         return false;
440                 }
441
442                 /* @todo Add a formatter
443                 $format = new FormatSVG( $metadata );
444                 $formatted = $format->getFormattedData();
445                 */
446
447                 // Sort fields into visible and collapsed
448                 $visibleFields = $this->visibleMetadataFields();
449
450                 $showMeta = false;
451                 foreach ( $metadata as $name => $value ) {
452                         $tag = strtolower( $name );
453                         if ( isset( self::$metaConversion[$tag] ) ) {
454                                 $tag = strtolower( self::$metaConversion[$tag] );
455                         } else {
456                                 // Do not output other metadata not in list
457                                 continue;
458                         }
459                         $showMeta = true;
460                         self::addMeta( $result,
461                                 in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
462                                 'exif',
463                                 $tag,
464                                 $value
465                         );
466                 }
467
468                 return $showMeta ? $result : false;
469         }
470
471         /**
472          * @param string $name Parameter name
473          * @param mixed $value Parameter value
474          * @return bool Validity
475          */
476         public function validateParam( $name, $value ) {
477                 if ( in_array( $name, [ 'width', 'height' ] ) ) {
478                         // Reject negative heights, widths
479                         return ( $value > 0 );
480                 } elseif ( $name == 'lang' ) {
481                         // Validate $code
482                         if ( $value === '' || !Language::isValidBuiltInCode( $value ) ) {
483                                 wfDebug( "Invalid user language code\n" );
484
485                                 return false;
486                         }
487
488                         return true;
489                 }
490
491                 // Only lang, width and height are acceptable keys
492                 return false;
493         }
494
495         /**
496          * @param array $params Name=>value pairs of parameters
497          * @return string Filename to use
498          */
499         public function makeParamString( $params ) {
500                 $lang = '';
501                 if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
502                         $params['lang'] = strtolower( $params['lang'] );
503                         $lang = "lang{$params['lang']}-";
504                 }
505                 if ( !isset( $params['width'] ) ) {
506                         return false;
507                 }
508
509                 return "$lang{$params['width']}px";
510         }
511
512         public function parseParamString( $str ) {
513                 $m = false;
514                 if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
515                         return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
516                 } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
517                         return [ 'width' => $m[1], 'lang' => 'en' ];
518                 } else {
519                         return false;
520                 }
521         }
522
523         public function getParamMap() {
524                 return [ 'img_lang' => 'lang', 'img_width' => 'width' ];
525         }
526
527         /**
528          * @param array $params
529          * @return array
530          */
531         function getScriptParams( $params ) {
532                 $scriptParams = [ 'width' => $params['width'] ];
533                 if ( isset( $params['lang'] ) ) {
534                         $scriptParams['lang'] = $params['lang'];
535                 }
536
537                 return $scriptParams;
538         }
539
540         public function getCommonMetaArray( File $file ) {
541                 $metadata = $file->getMetadata();
542                 if ( !$metadata ) {
543                         return [];
544                 }
545                 $metadata = $this->unpackMetadata( $metadata );
546                 if ( !$metadata || isset( $metadata['error'] ) ) {
547                         return [];
548                 }
549                 $stdMetadata = [];
550                 foreach ( $metadata as $name => $value ) {
551                         $tag = strtolower( $name );
552                         if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
553                                 // Skip these. In the exif metadata stuff, it is assumed these
554                                 // are measured in px, which is not the case here.
555                                 continue;
556                         }
557                         if ( isset( self::$metaConversion[$tag] ) ) {
558                                 $tag = self::$metaConversion[$tag];
559                                 $stdMetadata[$tag] = $value;
560                         }
561                 }
562
563                 return $stdMetadata;
564         }
565 }