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