MediaWiki master
JpegHandler.php
Go to the documentation of this file.
1<?php
14
25 private const SRGB_EXIF_COLOR_SPACE = 'sRGB';
26 private const SRGB_ICC_PROFILE_DESCRIPTION = 'sRGB IEC61966-2.1';
27
29 public function normaliseParams( $image, &$params ) {
30 if ( !parent::normaliseParams( $image, $params ) ) {
31 return false;
32 }
33 if ( isset( $params['quality'] ) && !self::validateQuality( $params['quality'] ) ) {
34 return false;
35 }
36 return true;
37 }
38
40 public function validateParam( $name, $value ) {
41 if ( $name === 'quality' ) {
42 return self::validateQuality( $value );
43 }
44 return parent::validateParam( $name, $value );
45 }
46
51 private static function validateQuality( $value ) {
52 return $value === 'low';
53 }
54
56 public function makeParamString( $params ) {
57 // Prepend quality as "qValue-". This has to match parseParamString() below
58 $res = parent::makeParamString( $params );
59 if ( $res && isset( $params['quality'] ) ) {
60 $res = "q{$params['quality']}-$res";
61 }
62 return $res;
63 }
64
66 public function parseParamString( $str ) {
67 // $str contains "qlow-200px" or "200px" strings because thumb.php would strip the filename
68 // first - check if the string begins with "qlow-", and if so, treat it as quality.
69 // Pass the first portion, or the whole string if "qlow-" not found, to the parent
70 // The parsing must match the makeParamString() above
71 $res = false;
72 $m = false;
73 if ( preg_match( '/q([^-]+)-(.*)$/', $str, $m ) ) {
74 $v = $m[1];
75 if ( self::validateQuality( $v ) ) {
76 $res = parent::parseParamString( $m[2] );
77 if ( $res ) {
78 $res['quality'] = $v;
79 }
80 }
81 } else {
82 $res = parent::parseParamString( $str );
83 }
84 return $res;
85 }
86
88 protected function getScriptParams( $params ) {
89 $res = parent::getScriptParams( $params );
90 if ( isset( $params['quality'] ) ) {
91 $res['quality'] = $params['quality'];
92 }
93 return $res;
94 }
95
97 public function getSizeAndMetadata( $state, $filename ) {
98 try {
99 $meta = BitmapMetadataHandler::Jpeg( $filename );
100 if ( !is_array( $meta ) ) {
101 // This should never happen, but doesn't hurt to be paranoid.
102 throw new InvalidJpegException( 'Metadata array is not an array' );
103 }
104 $meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
105
106 $info = [
107 'width' => $meta['SOF']['width'] ?? 0,
108 'height' => $meta['SOF']['height'] ?? 0,
109 ];
110 if ( isset( $meta['SOF']['bits'] ) ) {
111 $info['bits'] = $meta['SOF']['bits'];
112 }
113 $info = $this->applyExifRotation( $info, $meta );
114 unset( $meta['SOF'] );
115 $info['metadata'] = $meta;
116 return $info;
117 } catch ( InvalidJpegException $e ) {
118 wfDebug( __METHOD__ . ': ' . $e->getMessage() );
119
120 // This used to return an integer-like string from getMetadata(),
121 // producing a value which could not be unserialized in
122 // img_metadata. The "_error" array key matches the legacy
123 // unserialization for such image rows.
124 return [ 'metadata' => [ '_error' => ExifBitmapHandler::BROKEN_FILE ] ];
125 }
126 }
127
135 public function rotate( $file, $params ) {
136 $jpegTran = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::JpegTran );
137
138 $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
139
140 if ( $jpegTran && is_executable( $jpegTran ) ) {
141 $command = Shell::command( $jpegTran,
142 '-rotate',
143 (string)$rotation,
144 '-outfile',
145 $params['dstPath'],
146 $params['srcPath']
147 );
148 $result = $command
149 ->includeStderr()
150 ->execute();
151 if ( $result->getExitCode() !== 0 ) {
152 $this->logErrorForExternalProcess( $result->getExitCode(),
153 $result->getStdout(),
154 $command->__toString()
155 );
156
157 return new MediaTransformError( 'thumbnail_error', 0, 0, $result->getStdout() );
158 }
159
160 return false;
161 }
162 return parent::rotate( $file, $params );
163 }
164
166 public function supportsBucketing() {
167 return true;
168 }
169
171 public function sanitizeParamsForBucketing( $params ) {
172 $params = parent::sanitizeParamsForBucketing( $params );
173
174 // Quality needs to be cleared for bucketing. Buckets need to be default quality
175 unset( $params['quality'] );
176
177 return $params;
178 }
179
183 protected function transformImageMagick( $image, $params ) {
184 $useTinyRGBForJPGThumbnails = MediaWikiServices::getInstance()
185 ->getMainConfig()->get( MainConfigNames::UseTinyRGBForJPGThumbnails );
186
187 $ret = parent::transformImageMagick( $image, $params );
188
189 if ( $ret ) {
190 return $ret;
191 }
192
193 if ( $useTinyRGBForJPGThumbnails ) {
194 // T100976 If the profile embedded in the JPG is sRGB, swap it for the smaller
195 // (and free) TinyRGB
196
206 $colorSpaces = [ self::SRGB_EXIF_COLOR_SPACE, '-' ];
207 $profiles = [ self::SRGB_ICC_PROFILE_DESCRIPTION ];
208
209 // we'll also add TinyRGB profile to images lacking a profile, but
210 // only if they're not low quality (which are meant to save bandwidth
211 // and we don't want to increase the filesize by adding a profile)
212 if ( isset( $params['quality'] ) && $params['quality'] > 30 ) {
213 $profiles[] = '-';
214 }
215
216 $this->swapICCProfile(
217 $params['dstPath'],
218 $colorSpaces,
219 $profiles,
220 realpath( __DIR__ ) . '/tinyrgb.icc'
221 );
222 }
223
224 return false;
225 }
226
238 public function swapICCProfile( $filepath, array $colorSpaces,
239 array $oldProfileStrings, $profileFilepath
240 ) {
241 $exiftool = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::Exiftool );
242
243 if ( !$exiftool || !is_executable( $exiftool ) ) {
244 return false;
245 }
246
247 $result = Shell::command(
248 $exiftool,
249 '-EXIF:ColorSpace',
250 '-ICC_Profile:ProfileDescription',
251 '-S',
252 '-T',
253 $filepath
254 )
255 ->includeStderr()
256 ->execute();
257
258 // Explode EXIF data into an array with [0 => Color Space, 1 => Device Model Desc]
259 $data = explode( "\t", trim( $result->getStdout() ), 3 );
260
261 if ( $result->getExitCode() !== 0 ) {
262 return false;
263 }
264
265 // Make a regex out of the source data to match it to an array of color
266 // spaces in a case-insensitive way
267 $colorSpaceRegex = '/' . preg_quote( $data[0], '/' ) . '/i';
268 if ( !preg_grep( $colorSpaceRegex, $colorSpaces ) ) {
269 // We can't establish that this file matches the color space, don't process it
270 return false;
271 }
272
273 $profileRegex = '/' . preg_quote( $data[1], '/' ) . '/i';
274 if ( !preg_grep( $profileRegex, $oldProfileStrings ) ) {
275 // We can't establish that this file has the expected ICC profile, don't process it
276 return false;
277 }
278
279 $command = Shell::command( $exiftool,
280 '-overwrite_original',
281 '-icc_profile<=' . $profileFilepath,
282 $filepath
283 );
284 $result = $command
285 ->includeStderr()
286 ->execute();
287
288 if ( $result->getExitCode() !== 0 ) {
289 $this->logErrorForExternalProcess( $result->getExitCode(),
290 $result->getStdout(),
291 $command->__toString()
292 );
293
294 return false;
295 }
296
297 return true;
298 }
299}
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:700
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)
to override array
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.If this returns null, the caller will fall back to getI...
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)
to override bool
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:79
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Executes shell commands.
Definition Shell.php:32