1<?php
2/**
3 * Handler for PNG 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 PNG images.
26 *
27 * @ingroup Media
28 */
29class PNGHandler extends BitmapHandler {
30	private const BROKEN_FILE = '0';
31
32	/**
33	 * @param File|FSFile $image
34	 * @param string $filename
35	 * @return string
36	 */
37	public function getMetadata( $image, $filename ) {
38		try {
39			$metadata = BitmapMetadataHandler::PNG( $filename );
40		} catch ( Exception $e ) {
41			// Broken file?
42			wfDebug( __METHOD__ . ': ' . $e->getMessage() );
43
44			return self::BROKEN_FILE;
45		}
46
47		return serialize( $metadata );
48	}
49
50	/**
51	 * @param File $image
52	 * @param IContextSource|false $context
53	 * @return array[]|false
54	 */
55	public function formatMetadata( $image, $context = false ) {
56		$meta = $this->getCommonMetaArray( $image );
57		if ( !$meta ) {
58			return false;
59		}
60
61		return $this->formatMetadataHelper( $meta, $context );
62	}
63
64	/**
65	 * Get a file type independent array of metadata.
66	 *
67	 * @param File $image
68	 * @return array The metadata array
69	 */
70	public function getCommonMetaArray( File $image ) {
71		$meta = $image->getMetadata();
72
73		if ( !$meta ) {
74			return [];
75		}
76		$meta = unserialize( $meta );
77		if ( !isset( $meta['metadata'] ) ) {
78			return [];
79		}
80		unset( $meta['metadata']['_MW_PNG_VERSION'] );
81
82		return $meta['metadata'];
83	}
84
85	/**
86	 * @param File $image
87	 * @return bool
88	 */
89	public function isAnimatedImage( $image ) {
90		$ser = $image->getMetadata();
91		if ( $ser ) {
92			$metadata = unserialize( $ser );
93			if ( $metadata['frameCount'] > 1 ) {
94				return true;
95			}
96		}
97
98		return false;
99	}
100
101	/**
102	 * We do not support making APNG thumbnails, so always false
103	 * @param File $image
104	 * @return bool False
105	 */
106	public function canAnimateThumbnail( $image ) {
107		return false;
108	}
109
110	public function getMetadataType( $image ) {
111		return 'parsed-png';
112	}
113
114	public function isMetadataValid( $image, $metadata ) {
115		if ( $metadata === self::BROKEN_FILE ) {
116			// Do not repetitivly regenerate metadata on broken file.
117			return self::METADATA_GOOD;
118		}
119
120		Wikimedia\suppressWarnings();
121		$data = unserialize( $metadata );
122		Wikimedia\restoreWarnings();
123
124		if ( !$data || !is_array( $data ) ) {
125			wfDebug( __METHOD__ . " invalid png metadata" );
126
127			return self::METADATA_BAD;
128		}
129
130		if ( !isset( $data['metadata']['_MW_PNG_VERSION'] )
131			|| $data['metadata']['_MW_PNG_VERSION'] != PNGMetadataExtractor::VERSION
132		) {
133			wfDebug( __METHOD__ . " old but compatible png metadata" );
134
135			return self::METADATA_COMPATIBLE;
136		}
137
138		return self::METADATA_GOOD;
139	}
140
141	/**
142	 * @param File $image
143	 * @return string
144	 */
145	public function getLongDesc( $image ) {
146		global $wgLang;
147		$original = parent::getLongDesc( $image );
148
149		Wikimedia\suppressWarnings();
150		$metadata = unserialize( $image->getMetadata() );
151		Wikimedia\restoreWarnings();
152
153		if ( !$metadata || $metadata['frameCount'] <= 0 ) {
154			return $original;
155		}
156
157		$info = [];
158		$info[] = $original;
159
160		if ( $metadata['loopCount'] == 0 ) {
161			$info[] = wfMessage( 'file-info-png-looped' )->parse();
162		} elseif ( $metadata['loopCount'] > 1 ) {
163			$info[] = wfMessage( 'file-info-png-repeat' )->numParams( $metadata['loopCount'] )->parse();
164		}
165
166		if ( $metadata['frameCount'] > 0 ) {
167			$info[] = wfMessage( 'file-info-png-frames' )->numParams( $metadata['frameCount'] )->parse();
168		}
169
170		if ( $metadata['duration'] ) {
171			$info[] = $wgLang->formatTimePeriod( $metadata['duration'] );
172		}
173
174		return $wgLang->commaList( $info );
175	}
176
177	/**
178	 * Return the duration of an APNG file.
179	 *
180	 * Shown in the &query=imageinfo&iiprop=size api query.
181	 *
182	 * @param File $file
183	 * @return float The duration of the file.
184	 */
185	public function getLength( $file ) {
186		$serMeta = $file->getMetadata();
187		Wikimedia\suppressWarnings();
188		$metadata = unserialize( $serMeta );
189		Wikimedia\restoreWarnings();
190
191		if ( !$metadata || !isset( $metadata['duration'] ) || !$metadata['duration'] ) {
192			return 0.0;
193		} else {
194			return (float)$metadata['duration'];
195		}
196	}
197
198	// PNGs should be easy to support, but it will need some sharpening applied
199	// and another user test to check if the perceived quality change is noticeable
200	public function supportsBucketing() {
201		return false;
202	}
203}
204