Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
38.84% covered (danger)
38.84%
47 / 121
11.76% covered (danger)
11.76%
2 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageHandler
39.17% covered (danger)
39.17%
47 / 120
11.76% covered (danger)
11.76%
2 / 17
660.74
0.00% covered (danger)
0.00%
0 / 1
 canRender
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getParamMap
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateParam
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 makeParamString
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 parseParamString
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getScriptParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 normaliseParams
75.00% covered (warning)
75.00%
21 / 28
0.00% covered (danger)
0.00%
0 / 1
15.64
 getSteppedThumbWidth
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
 validateThumbParams
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
5.50
 getScriptedTransform
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getImageSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSizeAndMetadata
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getImageArea
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getShortDesc
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getLongDesc
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 getDimensionsString
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 sanitizeParamsForBucketing
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Media-handling base classes and generic functionality.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup Media
8 */
9
10namespace MediaWiki\Media;
11
12use MediaWiki\FileRepo\File\File;
13use MediaWiki\MainConfigNames;
14use MediaWiki\MediaWikiServices;
15
16/**
17 * Media handler abstract base class for images
18 *
19 * @stable to extend
20 *
21 * @ingroup Media
22 */
23abstract class ImageHandler extends MediaHandler {
24    /**
25     * @inheritDoc
26     * @stable to override
27     * @param File $file
28     * @return bool
29     */
30    public function canRender( $file ) {
31        return ( $file->getWidth() && $file->getHeight() );
32    }
33
34    /**
35     * @inheritDoc
36     * @stable to override
37     * @return array<string,string>
38     */
39    public function getParamMap() {
40        return [ 'img_width' => 'width' ];
41    }
42
43    /**
44     * @inheritDoc
45     * @stable to override
46     */
47    public function validateParam( $name, $value ) {
48        return in_array( $name, [ 'width', 'height' ] ) && (int)$value > 0;
49    }
50
51    /**
52     * @inheritDoc
53     * @stable to override
54     * @throws MediaTransformInvalidParametersException
55     */
56    public function makeParamString( $params ) {
57        if ( isset( $params['physicalWidth'] ) ) {
58            $width = $params['physicalWidth'];
59        } elseif ( isset( $params['width'] ) ) {
60            $width = $params['width'];
61        } else {
62            throw new MediaTransformInvalidParametersException( 'No width specified to ' . __METHOD__ );
63        }
64
65        # Removed for ProofreadPage
66        # $width = intval( $width );
67        return "{$width}px";
68    }
69
70    /**
71     * @inheritDoc
72     * @stable to override
73     */
74    public function parseParamString( $str ) {
75        $m = false;
76        if ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
77            return [ 'width' => $m[1] ];
78        }
79        return false;
80    }
81
82    /**
83     * @stable to override
84     * @param array $params
85     * @return array
86     */
87    protected function getScriptParams( $params ) {
88        return [ 'width' => $params['width'] ];
89    }
90
91    /**
92     * @inheritDoc
93     * @stable to override
94     * @param File $image
95     * @param array &$params @phan-ignore-reference
96     * @return bool
97     * @phan-assert array{width:int,physicalWidth:int,height:int,physicalHeight:int,page:int} $params
98     */
99    public function normaliseParams( $image, &$params ) {
100        if ( !isset( $params['width'] ) ) {
101            return false;
102        }
103
104        if ( !isset( $params['page'] ) ) {
105            $params['page'] = 1;
106        } else {
107            $params['page'] = (int)$params['page'];
108            if ( $params['page'] > $image->pageCount() ) {
109                $params['page'] = $image->pageCount();
110            }
111
112            if ( $params['page'] < 1 ) {
113                $params['page'] = 1;
114            }
115        }
116
117        $srcWidth = $image->getWidth( $params['page'] );
118        $srcHeight = $image->getHeight( $params['page'] );
119
120        if ( isset( $params['height'] ) && $params['height'] !== -1 ) {
121            # Height & width were both set
122            if ( $params['width'] * $srcHeight > $params['height'] * $srcWidth ) {
123                # Height is the relative smaller dimension, so scale width accordingly
124                $params['width'] = self::fitBoxWidth( $srcWidth, $srcHeight, $params['height'] );
125
126                if ( $params['width'] == 0 ) {
127                    # Very small image, so we need to rely on client side scaling :(
128                    $params['width'] = 1;
129                }
130
131                $params['physicalWidth'] = $params['width'];
132            } else {
133                # Height was crap, unset it so that it will be calculated later
134                unset( $params['height'] );
135            }
136        }
137
138        if ( !isset( $params['physicalWidth'] ) ) {
139            # Passed all validations, so set the physicalWidth
140            $params['physicalWidth'] = $params['width'];
141        }
142
143        # Because thumbs are only referred to by width, the height always needs
144        # to be scaled by the width to keep the thumbnail sizes consistent,
145        # even if it was set inside the if block above
146        $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight,
147            $params['physicalWidth'] );
148
149        # Set the height if it was not validated in the if block higher up
150        if ( !isset( $params['height'] ) || $params['height'] === -1 ) {
151            $params['height'] = $params['physicalHeight'];
152        }
153
154        if ( !$this->validateThumbParams( $params['physicalWidth'],
155            $params['physicalHeight'], $srcWidth, $srcHeight )
156        ) {
157            return false;
158        }
159
160        return true;
161    }
162
163    /**
164     * Adjust the thumbnail size to fit the width steps defined in config via
165     * $wgThumbnailSteps.
166     *
167     * This logic is duplicated client-side in mw.util.adjustThumbWidthForSteps.
168     *
169     * @see FileTest::testThumbNameSteps
170     * @since 1.46 (also backported to 1.43.7, 1.44.4, 1.45.2)
171     */
172    protected function getSteppedThumbWidth(
173        File $image, int $requestWidth, int $srcWidth, int $srcHeight
174    ): int {
175        $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
176        $thumbnailSteps = $mainConfig->get( MainConfigNames::ThumbnailSteps );
177
178        if ( !$thumbnailSteps ) {
179            return $requestWidth;
180        }
181
182        $prevStep = $thumbnailSteps[0];
183        foreach ( $thumbnailSteps as $widthStep ) {
184            if ( ( $widthStep > $srcWidth ) && !$image->isVectorized() ) {
185                if ( $this->mustRender( $image ) ) {
186                    // Not web-safe: Round down to previous step
187                    //
188                    // While unlikely, this tries to upscale an original that is smaller than the smallest step
189                    // (e.g. smaller than 20px if that's the smallest step configured), which is disallowed
190                    // by default in MediaWiki core with HTTP 400 (ThumbnailEntryPoint::generateThumbnail).
191                    return $prevStep;
192                } else {
193                    // Web-safe: Round up or down and use the original
194                    //
195                    // There was no step between the requested width and the original width
196                    // (so either a thumb between penultimate step and original,
197                    // or a thumb beyond original jpg but under penultimate step).
198                    //
199                    // NOTE: This thumb-at-original-width is replaced with the original
200                    // in a higher-level code path after this method runs.
201                    return $srcWidth;
202                }
203            }
204            if ( $widthStep == $requestWidth ) {
205                return $requestWidth;
206            }
207            if ( $widthStep > $requestWidth ) {
208                return $widthStep;
209            }
210            $prevStep = $widthStep;
211        }
212
213        // Respond with the largest step if it's too big.
214        return $prevStep;
215    }
216
217    /**
218     * Validate thumbnail parameters and fill in the correct height
219     *
220     * @param int &$width Specified width (input/output)
221     * @param int &$height Height (output only)
222     * @param int $srcWidth Width of the source image
223     * @param int $srcHeight Height of the source image
224     * @return bool False to indicate that an error should be returned to the user.
225     */
226    private function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight ) {
227        $width = (int)$width;
228
229        if ( $width <= 0 ) {
230            wfDebug( __METHOD__ . ": Invalid destination width: $width" );
231
232            return false;
233        }
234        if ( $srcWidth <= 0 ) {
235            wfDebug( __METHOD__ . ": Invalid source width: $srcWidth" );
236
237            return false;
238        }
239
240        $height = File::scaleHeight( $srcWidth, $srcHeight, $width );
241        if ( $height == 0 ) {
242            # Force height to be at least 1 pixel
243            $height = 1;
244        }
245
246        return true;
247    }
248
249    /**
250     * @inheritDoc
251     * @stable to override
252     * @param File $image
253     * @param string $script
254     * @param array $params
255     * @return MediaTransformOutput|false
256     */
257    public function getScriptedTransform( $image, $script, $params ) {
258        if ( !$this->normaliseParams( $image, $params ) ) {
259            return false;
260        }
261        $url = wfAppendQuery( $script, $this->getScriptParams( $params ) );
262
263        if ( $image->mustRender() || $params['width'] < $image->getWidth() ) {
264            return new ThumbnailImage( $image, $url, false, $params );
265        }
266    }
267
268    /** @inheritDoc */
269    public function getImageSize( $image, $path ) {
270        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
271        return @getimagesize( $path );
272    }
273
274    /** @inheritDoc */
275    public function getSizeAndMetadata( $state, $path ) {
276        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
277        $gis = @getimagesize( $path );
278        if ( $gis ) {
279            $info = [
280                'width' => $gis[0],
281                'height' => $gis[1],
282            ];
283            if ( isset( $gis['bits'] ) ) {
284                $info['bits'] = $gis['bits'];
285            }
286        } else {
287            $info = [];
288        }
289        return $info;
290    }
291
292    /**
293     * Function that returns the number of pixels to be thumbnailed.
294     * Intended for animated GIFs to multiply by the number of frames.
295     *
296     * If the file doesn't support a notion of "area" return 0.
297     * @stable to override
298     *
299     * @param File $image
300     * @return int
301     */
302    public function getImageArea( $image ) {
303        return $image->getWidth() * $image->getHeight();
304    }
305
306    /**
307     * @inheritDoc
308     * @stable to override
309     * @param File $file
310     * @return string
311     */
312    public function getShortDesc( $file ) {
313        $lang = $this->getLanguage();
314        $nbytes = htmlspecialchars( $lang->formatSize( $file->getSize() ), ENT_QUOTES );
315        $widthheight = wfMessage( 'widthheight' )
316            ->numParams( $file->getWidth(), $file->getHeight() )
317            ->inLanguage( $lang )
318            ->escaped();
319
320        return "$widthheight ($nbytes)";
321    }
322
323    /**
324     * @inheritDoc
325     * @stable to override
326     * @param File $file
327     * @return string
328     */
329    public function getLongDesc( $file ) {
330        $pages = $file->pageCount();
331        if ( $pages === false || $pages <= 1 ) {
332            $msg = wfMessage( 'file-info-size' )
333                ->numParams( $file->getWidth(), $file->getHeight() )
334                ->sizeParams( $file->getSize() )
335                ->params( '<span class="mime-type">' . $file->getMimeType() . '</span>' )
336                ->inLanguage( $this->getLanguage() )
337                ->parse();
338        } else {
339            $msg = wfMessage( 'file-info-size-pages' )
340                ->numParams( $file->getWidth(), $file->getHeight() )
341                ->sizeParams( $file->getSize() )
342                ->params( '<span class="mime-type">' . $file->getMimeType() . '</span>' )->numParams( $pages )
343                ->inLanguage( $this->getLanguage() )
344                ->parse();
345        }
346
347        return $msg;
348    }
349
350    /**
351     * @inheritDoc
352     * @stable to override
353     * @param File $file
354     * @return string
355     */
356    public function getDimensionsString( $file ) {
357        $pages = $file->pageCount();
358        $lang = $this->getLanguage();
359        if ( $pages > 1 ) {
360            return wfMessage( 'widthheightpage' )
361                ->numParams( $file->getWidth(), $file->getHeight(), $pages )
362                ->inLanguage( $lang )->text();
363        }
364        return wfMessage( 'widthheight' )
365            ->numParams( $file->getWidth(), $file->getHeight() )->inLanguage( $lang )->text();
366    }
367
368    /**
369     * @inheritDoc
370     * @stable to override
371     */
372    public function sanitizeParamsForBucketing( $params ) {
373        $params = parent::sanitizeParamsForBucketing( $params );
374
375        // We unset the height parameters in order to let normaliseParams recalculate them
376        // Otherwise there might be a height discrepancy
377        unset( $params['height'] );
378        unset( $params['physicalHeight'] );
379
380        return $params;
381    }
382}
383
384/** @deprecated class alias since 1.46 */
385class_alias( ImageHandler::class, 'ImageHandler' );