1<?php
2/**
3 * Handler for GIF 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 GIF images.
26 *
27 * @ingroup Media
28 */
29class GIFHandler extends BitmapHandler {
30	/**
31	 * Value to store in img_metadata if there was error extracting metadata
32	 */
33	private const BROKEN_FILE = '0';
34
35	public function getMetadata( $image, $filename ) {
36		try {
37			$parsedGIFMetadata = BitmapMetadataHandler::GIF( $filename );
38		} catch ( Exception $e ) {
39			// Broken file?
40			wfDebug( __METHOD__ . ': ' . $e->getMessage() );
41
42			return self::BROKEN_FILE;
43		}
44
45		return serialize( $parsedGIFMetadata );
46	}
47
48	/**
49	 * @param File $image
50	 * @param bool|IContextSource $context Context to use (optional)
51	 * @return array|bool
52	 */
53	public function formatMetadata( $image, $context = false ) {
54		$meta = $this->getCommonMetaArray( $image );
55		if ( count( $meta ) === 0 ) {
56			return false;
57		}
58
59		return $this->formatMetadataHelper( $meta, $context );
60	}
61
62	/**
63	 * Return the standard metadata elements for #filemetadata parser func.
64	 * @param File $image
65	 * @return array|bool
66	 */
67	public function getCommonMetaArray( File $image ) {
68		$meta = $image->getMetadata();
69
70		if ( !$meta ) {
71			return [];
72		}
73		$meta = unserialize( $meta );
74		if ( !isset( $meta['metadata'] ) ) {
75			return [];
76		}
77		unset( $meta['metadata']['_MW_GIF_VERSION'] );
78
79		return $meta['metadata'];
80	}
81
82	/**
83	 * @todo Add unit tests
84	 *
85	 * @param File $image
86	 * @return bool
87	 */
88	public function getImageArea( $image ) {
89		$ser = $image->getMetadata();
90		if ( $ser ) {
91			$metadata = unserialize( $ser );
92			if ( isset( $metadata['frameCount'] ) && $metadata['frameCount'] > 0 ) {
93				return $image->getWidth() * $image->getHeight() * $metadata['frameCount'];
94			} else {
95				return $image->getWidth() * $image->getHeight();
96			}
97		} else {
98			return $image->getWidth() * $image->getHeight();
99		}
100	}
101
102	/**
103	 * @param File $image
104	 * @return bool
105	 */
106	public function isAnimatedImage( $image ) {
107		$ser = $image->getMetadata();
108		if ( $ser ) {
109			$metadata = unserialize( $ser );
110			if ( isset( $metadata['frameCount'] ) && $metadata['frameCount'] > 1 ) {
111				return true;
112			}
113		}
114
115		return false;
116	}
117
118	/**
119	 * We cannot animate thumbnails that are bigger than a particular size
120	 * @param File $file
121	 * @return bool
122	 */
123	public function canAnimateThumbnail( $file ) {
124		global $wgMaxAnimatedGifArea;
125
126		return $this->getImageArea( $file ) <= $wgMaxAnimatedGifArea;
127	}
128
129	public function getMetadataType( $image ) {
130		return 'parsed-gif';
131	}
132
133	public function isMetadataValid( $image, $metadata ) {
134		if ( $metadata === self::BROKEN_FILE ) {
135			// Do not repetitivly regenerate metadata on broken file.
136			return self::METADATA_GOOD;
137		}
138
139		Wikimedia\suppressWarnings();
140		$data = unserialize( $metadata );
141		Wikimedia\restoreWarnings();
142
143		if ( !$data || !is_array( $data ) ) {
144			wfDebug( __METHOD__ . " invalid GIF metadata" );
145
146			return self::METADATA_BAD;
147		}
148
149		if ( !isset( $data['metadata']['_MW_GIF_VERSION'] )
150			|| $data['metadata']['_MW_GIF_VERSION'] != GIFMetadataExtractor::VERSION
151		) {
152			wfDebug( __METHOD__ . " old but compatible GIF metadata" );
153
154			return self::METADATA_COMPATIBLE;
155		}
156
157		return self::METADATA_GOOD;
158	}
159
160	/**
161	 * @param File $image
162	 * @return string
163	 */
164	public function getLongDesc( $image ) {
165		global $wgLang;
166
167		$original = parent::getLongDesc( $image );
168
169		Wikimedia\suppressWarnings();
170		$metadata = unserialize( $image->getMetadata() );
171		Wikimedia\restoreWarnings();
172
173		if ( !$metadata || $metadata['frameCount'] <= 1 ) {
174			return $original;
175		}
176
177		/* Preserve original image info string, but strip the last char ')' so we can add even more */
178		$info = [];
179		$info[] = $original;
180
181		if ( $metadata['looped'] ) {
182			$info[] = wfMessage( 'file-info-gif-looped' )->parse();
183		}
184
185		if ( $metadata['frameCount'] > 1 ) {
186			$info[] = wfMessage( 'file-info-gif-frames' )->numParams( $metadata['frameCount'] )->parse();
187		}
188
189		if ( $metadata['duration'] ) {
190			$info[] = $wgLang->formatTimePeriod( $metadata['duration'] );
191		}
192
193		return $wgLang->commaList( $info );
194	}
195
196	/**
197	 * Return the duration of the GIF file.
198	 *
199	 * Shown in the &query=imageinfo&iiprop=size api query.
200	 *
201	 * @param File $file
202	 * @return float The duration of the file.
203	 */
204	public function getLength( $file ) {
205		$serMeta = $file->getMetadata();
206		Wikimedia\suppressWarnings();
207		$metadata = unserialize( $serMeta );
208		Wikimedia\restoreWarnings();
209
210		if ( !$metadata || !isset( $metadata['duration'] ) || !$metadata['duration'] ) {
211			return 0.0;
212		} else {
213			return (float)$metadata['duration'];
214		}
215	}
216}
217