]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/media/DjVu.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / media / DjVu.php
1 <?php
2 /**
3  * Handler for DjVu 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 DjVu images
26  *
27  * @ingroup Media
28  */
29 class DjVuHandler extends ImageHandler {
30         const EXPENSIVE_SIZE_LIMIT = 10485760; // 10MiB
31
32         /**
33          * @return bool
34          */
35         function isEnabled() {
36                 global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML;
37                 if ( !$wgDjvuRenderer || ( !$wgDjvuDump && !$wgDjvuToXML ) ) {
38                         wfDebug( "DjVu is disabled, please set \$wgDjvuRenderer and \$wgDjvuDump\n" );
39
40                         return false;
41                 } else {
42                         return true;
43                 }
44         }
45
46         /**
47          * @param File $file
48          * @return bool
49          */
50         public function mustRender( $file ) {
51                 return true;
52         }
53
54         /**
55          * True if creating thumbnails from the file is large or otherwise resource-intensive.
56          * @param File $file
57          * @return bool
58          */
59         public function isExpensiveToThumbnail( $file ) {
60                 return $file->getSize() > static::EXPENSIVE_SIZE_LIMIT;
61         }
62
63         /**
64          * @param File $file
65          * @return bool
66          */
67         public function isMultiPage( $file ) {
68                 return true;
69         }
70
71         /**
72          * @return array
73          */
74         public function getParamMap() {
75                 return [
76                         'img_width' => 'width',
77                         'img_page' => 'page',
78                 ];
79         }
80
81         /**
82          * @param string $name
83          * @param mixed $value
84          * @return bool
85          */
86         public function validateParam( $name, $value ) {
87                 if ( $name === 'page' && trim( $value ) !== (string)intval( $value ) ) {
88                         // Extra junk on the end of page, probably actually a caption
89                         // e.g. [[File:Foo.djvu|thumb|Page 3 of the document shows foo]]
90                         return false;
91                 }
92                 if ( in_array( $name, [ 'width', 'height', 'page' ] ) ) {
93                         if ( $value <= 0 ) {
94                                 return false;
95                         } else {
96                                 return true;
97                         }
98                 } else {
99                         return false;
100                 }
101         }
102
103         /**
104          * @param array $params
105          * @return bool|string
106          */
107         public function makeParamString( $params ) {
108                 $page = isset( $params['page'] ) ? $params['page'] : 1;
109                 if ( !isset( $params['width'] ) ) {
110                         return false;
111                 }
112
113                 return "page{$page}-{$params['width']}px";
114         }
115
116         /**
117          * @param string $str
118          * @return array|bool
119          */
120         public function parseParamString( $str ) {
121                 $m = false;
122                 if ( preg_match( '/^page(\d+)-(\d+)px$/', $str, $m ) ) {
123                         return [ 'width' => $m[2], 'page' => $m[1] ];
124                 } else {
125                         return false;
126                 }
127         }
128
129         /**
130          * @param array $params
131          * @return array
132          */
133         function getScriptParams( $params ) {
134                 return [
135                         'width' => $params['width'],
136                         'page' => $params['page'],
137                 ];
138         }
139
140         /**
141          * @param File $image
142          * @param string $dstPath
143          * @param string $dstUrl
144          * @param array $params
145          * @param int $flags
146          * @return MediaTransformError|ThumbnailImage|TransformParameterError
147          */
148         function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
149                 global $wgDjvuRenderer, $wgDjvuPostProcessor;
150
151                 if ( !$this->normaliseParams( $image, $params ) ) {
152                         return new TransformParameterError( $params );
153                 }
154                 $width = $params['width'];
155                 $height = $params['height'];
156                 $page = $params['page'];
157
158                 if ( $flags & self::TRANSFORM_LATER ) {
159                         $params = [
160                                 'width' => $width,
161                                 'height' => $height,
162                                 'page' => $page
163                         ];
164
165                         return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
166                 }
167
168                 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
169                         return new MediaTransformError(
170                                 'thumbnail_error',
171                                 $width,
172                                 $height,
173                                 wfMessage( 'thumbnail_dest_directory' )
174                         );
175                 }
176
177                 // Get local copy source for shell scripts
178                 // Thumbnail extraction is very inefficient for large files.
179                 // Provide a way to pool count limit the number of downloaders.
180                 if ( $image->getSize() >= 1e7 ) { // 10MB
181                         $work = new PoolCounterWorkViaCallback( 'GetLocalFileCopy', sha1( $image->getName() ),
182                                 [
183                                         'doWork' => function () use ( $image ) {
184                                                 return $image->getLocalRefPath();
185                                         }
186                                 ]
187                         );
188                         $srcPath = $work->execute();
189                 } else {
190                         $srcPath = $image->getLocalRefPath();
191                 }
192
193                 if ( $srcPath === false ) { // Failed to get local copy
194                         wfDebugLog( 'thumbnail',
195                                 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
196                                         wfHostname(), $image->getName() ) );
197
198                         return new MediaTransformError( 'thumbnail_error',
199                                 $params['width'], $params['height'],
200                                 wfMessage( 'filemissing' )
201                         );
202                 }
203
204                 # Use a subshell (brackets) to aggregate stderr from both pipeline commands
205                 # before redirecting it to the overall stdout. This works in both Linux and Windows XP.
206                 $cmd = '(' . wfEscapeShellArg(
207                         $wgDjvuRenderer,
208                         "-format=ppm",
209                         "-page={$page}",
210                         "-size={$params['physicalWidth']}x{$params['physicalHeight']}",
211                         $srcPath );
212                 if ( $wgDjvuPostProcessor ) {
213                         $cmd .= " | {$wgDjvuPostProcessor}";
214                 }
215                 $cmd .= ' > ' . wfEscapeShellArg( $dstPath ) . ') 2>&1';
216                 wfDebug( __METHOD__ . ": $cmd\n" );
217                 $retval = '';
218                 $err = wfShellExec( $cmd, $retval );
219
220                 $removed = $this->removeBadFile( $dstPath, $retval );
221                 if ( $retval != 0 || $removed ) {
222                         $this->logErrorForExternalProcess( $retval, $err, $cmd );
223                         return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
224                 } else {
225                         $params = [
226                                 'width' => $width,
227                                 'height' => $height,
228                                 'page' => $page
229                         ];
230
231                         return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
232                 }
233         }
234
235         /**
236          * Cache an instance of DjVuImage in an Image object, return that instance
237          *
238          * @param File|FSFile $image
239          * @param string $path
240          * @return DjVuImage
241          */
242         function getDjVuImage( $image, $path ) {
243                 if ( !$image ) {
244                         $deja = new DjVuImage( $path );
245                 } elseif ( !isset( $image->dejaImage ) ) {
246                         $deja = $image->dejaImage = new DjVuImage( $path );
247                 } else {
248                         $deja = $image->dejaImage;
249                 }
250
251                 return $deja;
252         }
253
254         /**
255          * Get metadata, unserializing it if neccessary.
256          *
257          * @param File $file The DjVu file in question
258          * @return string XML metadata as a string.
259          * @throws MWException
260          */
261         private function getUnserializedMetadata( File $file ) {
262                 $metadata = $file->getMetadata();
263                 if ( substr( $metadata, 0, 3 ) === '<?xml' ) {
264                         // Old style. Not serialized but instead just a raw string of XML.
265                         return $metadata;
266                 }
267
268                 MediaWiki\suppressWarnings();
269                 $unser = unserialize( $metadata );
270                 MediaWiki\restoreWarnings();
271                 if ( is_array( $unser ) ) {
272                         if ( isset( $unser['error'] ) ) {
273                                 return false;
274                         } elseif ( isset( $unser['xml'] ) ) {
275                                 return $unser['xml'];
276                         } else {
277                                 // Should never ever reach here.
278                                 throw new MWException( "Error unserializing DjVu metadata." );
279                         }
280                 }
281
282                 // unserialize failed. Guess it wasn't really serialized after all,
283                 return $metadata;
284         }
285
286         /**
287          * Cache a document tree for the DjVu XML metadata
288          * @param File $image
289          * @param bool $gettext DOCUMENT (Default: false)
290          * @return bool|SimpleXMLElement
291          */
292         public function getMetaTree( $image, $gettext = false ) {
293                 if ( $gettext && isset( $image->djvuTextTree ) ) {
294                         return $image->djvuTextTree;
295                 }
296                 if ( !$gettext && isset( $image->dejaMetaTree ) ) {
297                         return $image->dejaMetaTree;
298                 }
299
300                 $metadata = $this->getUnserializedMetadata( $image );
301                 if ( !$this->isMetadataValid( $image, $metadata ) ) {
302                         wfDebug( "DjVu XML metadata is invalid or missing, should have been fixed in upgradeRow\n" );
303
304                         return false;
305                 }
306
307                 $trees = $this->extractTreesFromMetadata( $metadata );
308                 $image->djvuTextTree = $trees['TextTree'];
309                 $image->dejaMetaTree = $trees['MetaTree'];
310
311                 if ( $gettext ) {
312                         return $image->djvuTextTree;
313                 } else {
314                         return $image->dejaMetaTree;
315                 }
316         }
317
318         /**
319          * Extracts metadata and text trees from metadata XML in string form
320          * @param string $metadata XML metadata as a string
321          * @return array
322          */
323         protected function extractTreesFromMetadata( $metadata ) {
324                 MediaWiki\suppressWarnings();
325                 try {
326                         // Set to false rather than null to avoid further attempts
327                         $metaTree = false;
328                         $textTree = false;
329                         $tree = new SimpleXMLElement( $metadata, LIBXML_PARSEHUGE );
330                         if ( $tree->getName() == 'mw-djvu' ) {
331                                 /** @var SimpleXMLElement $b */
332                                 foreach ( $tree->children() as $b ) {
333                                         if ( $b->getName() == 'DjVuTxt' ) {
334                                                 // @todo File::djvuTextTree and File::dejaMetaTree are declared
335                                                 // dynamically. Add a public File::$data to facilitate this?
336                                                 $textTree = $b;
337                                         } elseif ( $b->getName() == 'DjVuXML' ) {
338                                                 $metaTree = $b;
339                                         }
340                                 }
341                         } else {
342                                 $metaTree = $tree;
343                         }
344                 } catch ( Exception $e ) {
345                         wfDebug( "Bogus multipage XML metadata\n" );
346                 }
347                 MediaWiki\restoreWarnings();
348
349                 return [ 'MetaTree' => $metaTree, 'TextTree' => $textTree ];
350         }
351
352         function getImageSize( $image, $path ) {
353                 return $this->getDjVuImage( $image, $path )->getImageSize();
354         }
355
356         function getThumbType( $ext, $mime, $params = null ) {
357                 global $wgDjvuOutputExtension;
358                 static $mime;
359                 if ( !isset( $mime ) ) {
360                         $magic = MimeMagic::singleton();
361                         $mime = $magic->guessTypesForExtension( $wgDjvuOutputExtension );
362                 }
363
364                 return [ $wgDjvuOutputExtension, $mime ];
365         }
366
367         function getMetadata( $image, $path ) {
368                 wfDebug( "Getting DjVu metadata for $path\n" );
369
370                 $xml = $this->getDjVuImage( $image, $path )->retrieveMetaData();
371                 if ( $xml === false ) {
372                         // Special value so that we don't repetitively try and decode a broken file.
373                         return serialize( [ 'error' => 'Error extracting metadata' ] );
374                 } else {
375                         return serialize( [ 'xml' => $xml ] );
376                 }
377         }
378
379         function getMetadataType( $image ) {
380                 return 'djvuxml';
381         }
382
383         function isMetadataValid( $image, $metadata ) {
384                 return !empty( $metadata ) && $metadata != serialize( [] );
385         }
386
387         function pageCount( File $image ) {
388                 $info = $this->getDimensionInfo( $image );
389
390                 return $info ? $info['pageCount'] : false;
391         }
392
393         function getPageDimensions( File $image, $page ) {
394                 $index = $page - 1; // MW starts pages at 1
395
396                 $info = $this->getDimensionInfo( $image );
397                 if ( $info && isset( $info['dimensionsByPage'][$index] ) ) {
398                         return $info['dimensionsByPage'][$index];
399                 }
400
401                 return false;
402         }
403
404         protected function getDimensionInfo( File $file ) {
405                 $cache = ObjectCache::getMainWANInstance();
406                 return $cache->getWithSetCallback(
407                         $cache->makeKey( 'file-djvu', 'dimensions', $file->getSha1() ),
408                         $cache::TTL_INDEFINITE,
409                         function () use ( $file ) {
410                                 $tree = $this->getMetaTree( $file );
411                                 return $this->getDimensionInfoFromMetaTree( $tree );
412                         },
413                         [ 'pcTTL' => $cache::TTL_INDEFINITE ]
414                 );
415         }
416
417         /**
418          * Given an XML metadata tree, returns dimension information about the document
419          * @param bool|SimpleXMLElement $metatree The file's XML metadata tree
420          * @return bool|array
421          */
422         protected function getDimensionInfoFromMetaTree( $metatree ) {
423                 if ( !$metatree ) {
424                         return false;
425                 }
426
427                 $dimsByPage = [];
428                 $count = count( $metatree->xpath( '//OBJECT' ) );
429                 for ( $i = 0; $i < $count; $i++ ) {
430                         $o = $metatree->BODY[0]->OBJECT[$i];
431                         if ( $o ) {
432                                 $dimsByPage[$i] = [
433                                         'width' => (int)$o['width'],
434                                         'height' => (int)$o['height'],
435                                 ];
436                         } else {
437                                 $dimsByPage[$i] = false;
438                         }
439                 }
440
441                 return [ 'pageCount' => $count, 'dimensionsByPage' => $dimsByPage ];
442         }
443
444         /**
445          * @param File $image
446          * @param int $page Page number to get information for
447          * @return bool|string Page text or false when no text found.
448          */
449         function getPageText( File $image, $page ) {
450                 $tree = $this->getMetaTree( $image, true );
451                 if ( !$tree ) {
452                         return false;
453                 }
454
455                 $o = $tree->BODY[0]->PAGE[$page - 1];
456                 if ( $o ) {
457                         $txt = $o['value'];
458
459                         return $txt;
460                 } else {
461                         return false;
462                 }
463         }
464 }