Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.55% covered (warning)
54.55%
42 / 77
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExifBitmapHandler
54.55% covered (warning)
54.55%
42 / 77
0.00% covered (danger)
0.00%
0 / 8
216.65
0.00% covered (danger)
0.00%
0 / 1
 convertMetadataVersion
80.00% covered (warning)
80.00%
20 / 25
0.00% covered (danger)
0.00%
0 / 1
19.31
 isFileMetadataValid
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
9.24
 formatMetadata
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getCommonMetaArray
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getMetadataType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 applyExifRotation
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getRotation
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getRotationForExifFromOrientation
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3/**
4 * Handler for bitmap images with exif metadata.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 * @ingroup Media
23 */
24
25use MediaWiki\Context\IContextSource;
26use MediaWiki\FileRepo\File\File;
27use MediaWiki\MainConfigNames;
28use MediaWiki\MediaWikiServices;
29
30/**
31 * Stuff specific to JPEG and (built-in) TIFF handler.
32 * All metadata related, since both JPEG and TIFF support Exif.
33 *
34 * @stable to extend
35 * @ingroup Media
36 */
37class ExifBitmapHandler extends BitmapHandler {
38    /** Error extracting metadata */
39    public const BROKEN_FILE = '-1';
40
41    /** Outdated error extracting metadata */
42    public const OLD_BROKEN_FILE = '0';
43
44    public function convertMetadataVersion( $metadata, $version = 1 ) {
45        // basically flattens arrays.
46        $version = is_int( $version ) ? $version : (int)explode( ';', $version, 2 )[0];
47        if ( $version < 1 || $version >= 2 ) {
48            return $metadata;
49        }
50
51        if ( !isset( $metadata['MEDIAWIKI_EXIF_VERSION'] ) || $metadata['MEDIAWIKI_EXIF_VERSION'] !== 2 ) {
52            return $metadata;
53        }
54
55        // Treat Software as a special case because in can contain
56        // an array of (SoftwareName, Version).
57        if ( isset( $metadata['Software'] )
58            && is_array( $metadata['Software'] )
59            && is_array( $metadata['Software'][0] )
60            && isset( $metadata['Software'][0][0] )
61            && isset( $metadata['Software'][0][1] )
62        ) {
63            $metadata['Software'] = $metadata['Software'][0][0] . ' (Version '
64                . $metadata['Software'][0][1] . ')';
65        }
66
67        $formatter = new FormatMetadata;
68
69        // ContactInfo also has to be dealt with specially
70        if ( isset( $metadata['Contact'] ) ) {
71            $metadata['Contact'] = $formatter->collapseContactInfo(
72                is_array( $metadata['Contact'] ) ? $metadata['Contact'] : [ $metadata['Contact'] ]
73            );
74        }
75
76        // Ignore Location shown if it is not a simple string
77        if ( isset( $metadata['LocationShown'] ) && !is_string( $metadata['LocationShown'] ) ) {
78            unset( $metadata['LocationShown'] );
79        }
80
81        foreach ( $metadata as &$val ) {
82            if ( is_array( $val ) ) {
83                // @phan-suppress-next-line SecurityCheck-DoubleEscaped Ambiguous with the true for nohtml
84                $val = $formatter->flattenArrayReal( $val, 'ul', true );
85            }
86        }
87        unset( $val );
88        $metadata['MEDIAWIKI_EXIF_VERSION'] = 1;
89
90        return $metadata;
91    }
92
93    /**
94     * @param File $image
95     * @return bool|int
96     */
97    public function isFileMetadataValid( $image ) {
98        $showEXIF = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ShowEXIF );
99        if ( !$showEXIF ) {
100            # Metadata disabled and so an empty field is expected
101            return self::METADATA_GOOD;
102        }
103        $exif = $image->getMetadataArray();
104        if ( !$exif ) {
105            wfDebug( __METHOD__ . ': error unserializing?' );
106            return self::METADATA_BAD;
107        }
108        if ( $exif === [ '_error' => self::OLD_BROKEN_FILE ] ) {
109            # Old special value indicating that there is no Exif data in the file.
110            # or that there was an error well extracting the metadata.
111            wfDebug( __METHOD__ . ": back-compat version" );
112            return self::METADATA_COMPATIBLE;
113        }
114
115        if ( $exif === [ '_error' => self::BROKEN_FILE ] ) {
116            return self::METADATA_GOOD;
117        }
118
119        if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] )
120            || $exif['MEDIAWIKI_EXIF_VERSION'] !== Exif::version()
121        ) {
122            if ( isset( $exif['MEDIAWIKI_EXIF_VERSION'] )
123                && $exif['MEDIAWIKI_EXIF_VERSION'] === 1
124            ) {
125                // back-compatible but old
126                wfDebug( __METHOD__ . ": back-compat version" );
127
128                return self::METADATA_COMPATIBLE;
129            }
130            # Wrong (non-compatible) version
131            wfDebug( __METHOD__ . ": wrong version" );
132
133            return self::METADATA_BAD;
134        }
135
136        return self::METADATA_GOOD;
137    }
138
139    /**
140     * @param File $image
141     * @param IContextSource|false $context
142     * @return array[]|false
143     */
144    public function formatMetadata( $image, $context = false ) {
145        $meta = $this->getCommonMetaArray( $image );
146        if ( !$meta ) {
147            return false;
148        }
149
150        return $this->formatMetadataHelper( $meta, $context );
151    }
152
153    public function getCommonMetaArray( File $file ) {
154        $exif = $file->getMetadataArray();
155        if ( !$exif ) {
156            return [];
157        }
158        unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
159
160        return $exif;
161    }
162
163    public function getMetadataType( $image ) {
164        return 'exif';
165    }
166
167    protected function applyExifRotation( $info, $metadata ) {
168        if ( $this->autoRotateEnabled() ) {
169            $rotation = $this->getRotationForExifFromOrientation( $metadata['Orientation'] ?? null );
170        } else {
171            $rotation = 0;
172        }
173
174        if ( $rotation === 90 || $rotation === 270 ) {
175            $width = $info['width'];
176            $info['width'] = $info['height'];
177            $info['height'] = $width;
178        }
179        return $info;
180    }
181
182    /**
183     * On supporting image formats, try to read out the low-level orientation
184     * of the file and return the angle that the file needs to be rotated to
185     * be viewed.
186     *
187     * This information is only useful when manipulating the original file;
188     * the width and height we normally work with is logical, and will match
189     * any produced output views.
190     *
191     * @param File $file
192     * @return int 0, 90, 180 or 270
193     */
194    public function getRotation( $file ) {
195        if ( !$this->autoRotateEnabled() ) {
196            return 0;
197        }
198
199        $orientation = $file->getMetadataItem( 'Orientation' );
200        return $this->getRotationForExifFromOrientation( $orientation );
201    }
202
203    /**
204     * Given a chunk of serialized Exif metadata, return the orientation as
205     * degrees of rotation.
206     *
207     * @param int|null $orientation
208     * @return int 0, 90, 180 or 270
209     * @todo FIXME: Orientation can include flipping as well; see if this is an issue!
210     */
211    protected function getRotationForExifFromOrientation( $orientation ) {
212        if ( $orientation === null ) {
213            return 0;
214        }
215        # See http://sylvana.net/jpegcrop/exif_orientation.html
216        switch ( $orientation ) {
217            case 8:
218                return 90;
219            case 3:
220                return 180;
221            case 6:
222                return 270;
223            default:
224                return 0;
225        }
226    }
227}