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