MediaWiki master
JpegHandler.php
Go to the documentation of this file.
1<?php
28
39 private const SRGB_EXIF_COLOR_SPACE = 'sRGB';
40 private const SRGB_ICC_PROFILE_DESCRIPTION = 'sRGB IEC61966-2.1';
41
42 public function normaliseParams( $image, &$params ) {
43 if ( !parent::normaliseParams( $image, $params ) ) {
44 return false;
45 }
46 if ( isset( $params['quality'] ) && !self::validateQuality( $params['quality'] ) ) {
47 return false;
48 }
49 return true;
50 }
51
52 public function validateParam( $name, $value ) {
53 if ( $name === 'quality' ) {
54 return self::validateQuality( $value );
55 }
56 return parent::validateParam( $name, $value );
57 }
58
63 private static function validateQuality( $value ) {
64 return $value === 'low';
65 }
66
67 public function makeParamString( $params ) {
68 // Prepend quality as "qValue-". This has to match parseParamString() below
69 $res = parent::makeParamString( $params );
70 if ( $res && isset( $params['quality'] ) ) {
71 $res = "q{$params['quality']}-$res";
72 }
73 return $res;
74 }
75
76 public function parseParamString( $str ) {
77 // $str contains "qlow-200px" or "200px" strings because thumb.php would strip the filename
78 // first - check if the string begins with "qlow-", and if so, treat it as quality.
79 // Pass the first portion, or the whole string if "qlow-" not found, to the parent
80 // The parsing must match the makeParamString() above
81 $res = false;
82 $m = false;
83 if ( preg_match( '/q([^-]+)-(.*)$/', $str, $m ) ) {
84 $v = $m[1];
85 if ( self::validateQuality( $v ) ) {
86 $res = parent::parseParamString( $m[2] );
87 if ( $res ) {
88 $res['quality'] = $v;
89 }
90 }
91 } else {
92 $res = parent::parseParamString( $str );
93 }
94 return $res;
95 }
96
97 protected function getScriptParams( $params ) {
98 $res = parent::getScriptParams( $params );
99 if ( isset( $params['quality'] ) ) {
100 $res['quality'] = $params['quality'];
101 }
102 return $res;
103 }
104
105 public function getSizeAndMetadata( $state, $filename ) {
106 try {
107 $meta = BitmapMetadataHandler::Jpeg( $filename );
108 if ( !is_array( $meta ) ) {
109 // This should never happen, but doesn't hurt to be paranoid.
110 throw new InvalidJpegException( 'Metadata array is not an array' );
111 }
112 $meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
113
114 $info = [
115 'width' => $meta['SOF']['width'] ?? 0,
116 'height' => $meta['SOF']['height'] ?? 0,
117 ];
118 if ( isset( $meta['SOF']['bits'] ) ) {
119 $info['bits'] = $meta['SOF']['bits'];
120 }
121 $info = $this->applyExifRotation( $info, $meta );
122 unset( $meta['SOF'] );
123 $info['metadata'] = $meta;
124 return $info;
125 } catch ( InvalidJpegException $e ) {
126 wfDebug( __METHOD__ . ': ' . $e->getMessage() );
127
128 // This used to return an integer-like string from getMetadata(),
129 // producing a value which could not be unserialized in
130 // img_metadata. The "_error" array key matches the legacy
131 // unserialization for such image rows.
132 return [ 'metadata' => [ '_error' => ExifBitmapHandler::BROKEN_FILE ] ];
133 }
134 }
135
143 public function rotate( $file, $params ) {
144 $jpegTran = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::JpegTran );
145
146 $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
147
148 if ( $jpegTran && is_executable( $jpegTran ) ) {
149 $command = Shell::command( $jpegTran,
150 '-rotate',
151 (string)$rotation,
152 '-outfile',
153 $params['dstPath'],
154 $params['srcPath']
155 );
156 $result = $command
157 ->includeStderr()
158 ->execute();
159 if ( $result->getExitCode() !== 0 ) {
160 $this->logErrorForExternalProcess( $result->getExitCode(),
161 $result->getStdout(),
162 $command
163 );
164
165 return new MediaTransformError( 'thumbnail_error', 0, 0, $result->getStdout() );
166 }
167
168 return false;
169 }
170 return parent::rotate( $file, $params );
171 }
172
173 public function supportsBucketing() {
174 return true;
175 }
176
177 public function sanitizeParamsForBucketing( $params ) {
178 $params = parent::sanitizeParamsForBucketing( $params );
179
180 // Quality needs to be cleared for bucketing. Buckets need to be default quality
181 unset( $params['quality'] );
182
183 return $params;
184 }
185
189 protected function transformImageMagick( $image, $params ) {
190 $useTinyRGBForJPGThumbnails = MediaWikiServices::getInstance()
191 ->getMainConfig()->get( MainConfigNames::UseTinyRGBForJPGThumbnails );
192
193 $ret = parent::transformImageMagick( $image, $params );
194
195 if ( $ret ) {
196 return $ret;
197 }
198
199 if ( $useTinyRGBForJPGThumbnails ) {
200 // T100976 If the profile embedded in the JPG is sRGB, swap it for the smaller
201 // (and free) TinyRGB
202
212 $colorSpaces = [ self::SRGB_EXIF_COLOR_SPACE, '-' ];
213 $profiles = [ self::SRGB_ICC_PROFILE_DESCRIPTION ];
214
215 // we'll also add TinyRGB profile to images lacking a profile, but
216 // only if they're not low quality (which are meant to save bandwidth
217 // and we don't want to increase the filesize by adding a profile)
218 if ( isset( $params['quality'] ) && $params['quality'] > 30 ) {
219 $profiles[] = '-';
220 }
221
222 $this->swapICCProfile(
223 $params['dstPath'],
224 $colorSpaces,
225 $profiles,
226 realpath( __DIR__ ) . '/tinyrgb.icc'
227 );
228 }
229
230 return false;
231 }
232
244 public function swapICCProfile( $filepath, array $colorSpaces,
245 array $oldProfileStrings, $profileFilepath
246 ) {
247 $exiftool = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::Exiftool );
248
249 if ( !$exiftool || !is_executable( $exiftool ) ) {
250 return false;
251 }
252
253 $result = Shell::command(
254 $exiftool,
255 '-EXIF:ColorSpace',
256 '-ICC_Profile:ProfileDescription',
257 '-S',
258 '-T',
259 $filepath
260 )
261 ->includeStderr()
262 ->execute();
263
264 // Explode EXIF data into an array with [0 => Color Space, 1 => Device Model Desc]
265 $data = explode( "\t", trim( $result->getStdout() ), 3 );
266
267 if ( $result->getExitCode() !== 0 ) {
268 return false;
269 }
270
271 // Make a regex out of the source data to match it to an array of color
272 // spaces in a case-insensitive way
273 $colorSpaceRegex = '/' . preg_quote( $data[0], '/' ) . '/i';
274 if ( !preg_grep( $colorSpaceRegex, $colorSpaces ) ) {
275 // We can't establish that this file matches the color space, don't process it
276 return false;
277 }
278
279 $profileRegex = '/' . preg_quote( $data[1], '/' ) . '/i';
280 if ( !preg_grep( $profileRegex, $oldProfileStrings ) ) {
281 // We can't establish that this file has the expected ICC profile, don't process it
282 return false;
283 }
284
285 $command = Shell::command( $exiftool,
286 '-overwrite_original',
287 '-icc_profile<=' . $profileFilepath,
288 $filepath
289 );
290 $result = $command
291 ->includeStderr()
292 ->execute();
293
294 if ( $result->getExitCode() !== 0 ) {
295 $this->logErrorForExternalProcess( $result->getExitCode(),
296 $result->getStdout(),
297 $command
298 );
299
300 return false;
301 }
302
303 return true;
304 }
305}
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
static Jpeg( $filename)
Main entry point for jpeg's.
Stuff specific to JPEG and (built-in) TIFF handler.
applyExifRotation( $info, $metadata)
getRotation( $file)
On supporting image formats, try to read out the low-level orientation of the file and return the ang...
const BROKEN_FILE
Error extracting metadata.
static version()
The version of the output format.
Definition Exif.php:704
JPEG specific handler.
makeParamString( $params)
Merge a parameter array into a string appropriate for inclusion in filenames.stringto override to ove...
validateParam( $name, $value)
Validate a thumbnail parameter at parse time.Return true to accept the parameter, and false to reject...
getScriptParams( $params)
swapICCProfile( $filepath, array $colorSpaces, array $oldProfileStrings, $profileFilepath)
Swaps an embedded ICC profile for another, if found.
getSizeAndMetadata( $state, $filename)
Get image size information and metadata array.
supportsBucketing()
Returns whether or not this handler supports the chained generation of thumbnails according to bucket...
rotate( $file, $params)
parseParamString( $str)
Parse a param string made with makeParamString back into an array.array|false Array of parameters or ...
sanitizeParamsForBucketing( $params)
Returns a normalised params array for which parameters have been cleaned up for bucketing purposes....
normaliseParams( $image, &$params)
transformImageMagick( $image, $params)
Transform an image using ImageMagick.to overrideMediaTransformError|false Error object if error occur...
logErrorForExternalProcess( $retval, $err, $cmd)
Log an error that occurred in an external process.
Basic media transform error class.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:93
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Executes shell commands.
Definition Shell.php:46