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