118 # TIFF Rev. 6.0 Attribute Information (p22)
120 # Tags relating to image structure
123 'BitsPerSample' => [
self::SHORT, 3 ], # Number
of bits per component
124 # "When a primary image is JPEG compressed, this designation is not"
125 # "necessary and is omitted." (p23)
126 'Compression' =>
self::SHORT, # Compression scheme #p23
127 'PhotometricInterpretation' =>
self::SHORT, # Pixel composition #p23
131 'YCbCrSubSampling' => [
self::SHORT, 2 ], # Subsampling ratio
of Y to C #p24
137 # Tags relating to recording offset
138 'StripOffsets' => self::SHORT_OR_LONG, #
Image data location
139 'RowsPerStrip' => self::SHORT_OR_LONG, # Number
of rows per strip
140 'StripByteCounts' => self::SHORT_OR_LONG, # Bytes per compressed strip
141 'JPEGInterchangeFormat' => self::SHORT_OR_LONG, # Offset to JPEG SOI
142 'JPEGInterchangeFormatLength' => self::SHORT_OR_LONG, # Bytes
of JPEG
data
144 # Tags relating to image
data characteristics
145 'TransferFunction' => self::IGNORE, # Transfer
function
146 'WhitePoint' => [ self::RATIONAL, 2 ], # White point chromaticity
147 'PrimaryChromaticities' => [ self::RATIONAL, 6 ], # Chromaticities
of primarities
148 # Color space transformation matrix coefficients #p27
149 'YCbCrCoefficients' => [ self::RATIONAL, 3 ],
150 'ReferenceBlackWhite' => [ self::RATIONAL, 6 ], # Pair
of black
and white reference
values
154 'ImageDescription' => self::ASCII, #
Image title
155 'Make' => self::ASCII, #
Image input equipment manufacturer
156 'Model' => self::ASCII, #
Image input equipment model
158 'Artist' => self::ASCII, # Person who created the image
159 'Copyright' => self::ASCII, #
Copyright holder
162 #
Exif IFD Attribute Information (p30-31)
164 # @todo NOTE: Nonexistence of this field is taken to mean nonconformance
165 # to the Exif 2.1 AND 2.2 standards
169 # Tags relating to Image Data Characteristics
170 'ColorSpace' =>
self::SHORT, # Color space information #p32
172 # Tags relating to image configuration
178 # Tags relating to related user information
182 # Tags relating to related file information
185 # Tags relating to date and time
189 'SubSecTimeOriginal' =>
self::ASCII, # DateTimeOriginal subseconds
190 'SubSecTimeDigitized' =>
self::ASCII, # DateTimeDigitized subseconds
192 # Tags relating to picture-taking conditions (p31)
196 'SpectralSensitivity' =>
self::ASCII, # Spectral sensitivity
197 'ISOSpeedRatings' =>
self::SHORT, # ISO speed rating
199 # Optoelectronic conversion factor. Note: We don't have support for this atm.
210 'SubjectArea' => [
self::SHORT, 4 ], # Subject area
212 'SpatialFrequencyResponse' =>
self::IGNORE, # Spatial frequency response. Not supported atm.
213 'FocalPlaneXResolution' =>
self::RATIONAL, # Focal plane X resolution
214 'FocalPlaneYResolution' =>
self::RATIONAL, # Focal plane Y resolution
215 'FocalPlaneResolutionUnit' =>
self::SHORT, # Focal plane resolution unit #p46
216 'SubjectLocation' => [
self::SHORT, 2 ], # Subject location
218 'SensingMethod' =>
self::SHORT, # Sensing method #p46
224 'WhiteBalance' =>
self::SHORT, # White Balance #p49
226 'FocalLengthIn35mmFilm' =>
self::SHORT, # Focal length
in 35 mm film
228 'GainControl' =>
self::SHORT, # Scene control #p49-50
233 # Device settings description. This could maybe be supported. Need to find an
234 # example file that uses this to see if it has stuff of interest in it.
235 'SubjectDistanceRange' =>
self::SHORT, # Subject distance range #p51
240 # GPS Attribute Information (p52)
243 # Should be an array of 4 Exif::BYTE's. However php treats it as an undefined
244 # Note exif standard calls this GPSVersionID, but php doesn't like the id suffix
245 'GPSLatitudeRef' =>
self::ASCII, # North
or South Latitude #p52-53
247 'GPSLongitudeRef' =>
self::ASCII, # East
or West Longitude #p53
250 # Altitude reference. Note, the exif standard says this should be an EXIF::Byte,
251 # but php seems to disagree.
253 'GPSTimeStamp' => [
self::RATIONAL, 3 ], # GPS time (atomic clock)
260 'GPSTrackRef' =>
self::ASCII, # Reference
for direction
of movement #p55
262 'GPSImgDirectionRef' =>
self::ASCII, # Reference
for direction
of image #p56
265 'GPSDestLatitudeRef' =>
self::ASCII, # Reference
for latitude
of destination #p56
266 'GPSDestLatitude' => [
self::RATIONAL, 3 ], # Latitude destination
267 'GPSDestLongitudeRef' =>
self::ASCII, # Reference
for longitude
of destination #p57
269 'GPSDestBearingRef' =>
self::ASCII, # Reference
for bearing
of destination #p57
271 'GPSDestDistanceRef' =>
self::ASCII, # Reference
for distance to destination #p57-58
276 'GPSDifferential' =>
self::SHORT, # GPS differential correction
288 wfWarn(
'Exif class did not have byte order specified. ' .
289 'Some properties may be decoded incorrectly.' );
290 $this->byteOrder =
'BE';
293 $this->
debugFile( $this->basename, __FUNCTION__,
true );
294 if ( function_exists(
'exif_read_data' ) ) {
295 MediaWiki\suppressWarnings();
296 $data = exif_read_data( $this->file, 0,
true );
297 MediaWiki\restoreWarnings();
299 throw new MWException(
"Internal error: exif_read_data not present. " .
300 "\$wgShowEXIF may be incorrectly set or not checked by an extension." );
307 $this->mRawExifData = $data ?: [];
317 $this->mFilteredExifData = [];
319 foreach ( array_keys( $this->mRawExifData )
as $section ) {
320 if ( !array_key_exists(
$section, $this->mExifTags ) ) {
321 $this->
debug( $section, __FUNCTION__,
"'$section' is not a valid Exif section" );
325 foreach ( array_keys( $this->mRawExifData[
$section] )
as $tag ) {
326 if ( !array_key_exists( $tag, $this->mExifTags[
$section] ) ) {
327 $this->
debug( $tag, __FUNCTION__,
"'$tag' is not a valid tag in '$section'" );
331 $this->mFilteredExifData[$tag] = $this->mRawExifData[
$section][$tag];
337 $this->
debug(
$value, __FUNCTION__,
"'$tag' contained invalid data" );
338 unset( $this->mFilteredExifData[$tag] );
368 if ( isset( $this->mFilteredExifData[
'GPSAltitude'] )
369 && isset( $this->mFilteredExifData[
'GPSAltitudeRef'] )
374 list( $num, $denom ) = explode(
'/', $this->mFilteredExifData[
'GPSAltitude'] );
375 $this->mFilteredExifData[
'GPSAltitude'] = $num / $denom;
377 if ( $this->mFilteredExifData[
'GPSAltitudeRef'] ===
"\1" ) {
378 $this->mFilteredExifData[
'GPSAltitude'] *= -1;
380 unset( $this->mFilteredExifData[
'GPSAltitudeRef'] );
393 if ( isset( $this->mFilteredExifData[
'ComponentsConfiguration'] ) ) {
394 $val = $this->mFilteredExifData[
'ComponentsConfiguration'];
397 $strLen = strlen( $val );
398 for ( $i = 0; $i < $strLen; $i++ ) {
399 $ccVals[$i] = ord( substr( $val, $i, 1 ) );
401 $ccVals[
'_type'] =
'ol';
402 $this->mFilteredExifData[
'ComponentsConfiguration'] = $ccVals;
412 if ( isset( $this->mFilteredExifData[
'GPSVersion'] ) ) {
413 $val = $this->mFilteredExifData[
'GPSVersion'];
416 $strLen = strlen( $val );
417 for ( $i = 0; $i < $strLen; $i++ ) {
421 $newVal .= ord( substr( $val, $i, 1 ) );
424 if ( $this->byteOrder ===
'LE' ) {
427 for ( $i = strlen( $newVal ) - 1; $i >= 0; $i-- ) {
428 $newVal2 .= substr( $newVal, $i, 1 );
430 $this->mFilteredExifData[
'GPSVersionID'] = $newVal2;
432 $this->mFilteredExifData[
'GPSVersionID'] = $newVal;
434 unset( $this->mFilteredExifData[
'GPSVersion'] );
445 if ( isset( $this->mFilteredExifData[$prop] ) ) {
446 if ( strlen( $this->mFilteredExifData[$prop] ) <= 8 ) {
449 $this->
debug( $this->mFilteredExifData[$prop], __FUNCTION__,
false );
450 unset( $this->mFilteredExifData[$prop] );
454 $charCode = substr( $this->mFilteredExifData[$prop], 0, 8 );
455 $val = substr( $this->mFilteredExifData[$prop], 8 );
457 switch ( $charCode ) {
458 case "\x4A\x49\x53\x00\x00\x00\x00\x00":
460 $charset =
"Shift-JIS";
470 MediaWiki\suppressWarnings();
471 $val = iconv( $charset,
'UTF-8//IGNORE', $val );
472 MediaWiki\restoreWarnings();
476 UtfNormal\Validator::quickIsNFCVerify( $valCopy );
477 if ( $valCopy !== $val ) {
478 MediaWiki\suppressWarnings();
479 $val = iconv(
'Windows-1252',
'UTF-8//IGNORE', $val );
480 MediaWiki\restoreWarnings();
486 if ( strlen( $val ) === 0 ) {
488 $this->
debug( $this->mFilteredExifData[$prop], __FUNCTION__,
"$prop: Is only whitespace" );
489 unset( $this->mFilteredExifData[$prop] );
495 $this->mFilteredExifData[$prop] = $val;
506 if ( isset( $this->mFilteredExifData[$prop] ) ) {
507 $this->mFilteredExifData[$prop] = ord( $this->mFilteredExifData[$prop] );
517 $loc =& $this->mFilteredExifData[$prop];
518 $dir =& $this->mFilteredExifData[$prop .
'Ref'];
521 if ( isset( $loc ) && isset(
$dir )
524 list( $num, $denom ) = explode(
'/', $loc[0] );
525 $res = $num / $denom;
526 list( $num, $denom ) = explode(
'/', $loc[1] );
527 $res += ( $num / $denom ) * ( 1 / 60 );
528 list( $num, $denom ) = explode(
'/', $loc[2] );
529 $res += ( $num / $denom ) * ( 1 / 3600 );
531 if (
$dir ===
'S' ||
$dir ===
'W' ) {
538 if (
$res !==
false ) {
539 $this->mFilteredExifData[$prop] =
$res;
540 unset( $this->mFilteredExifData[$prop .
'Ref'] );
542 unset( $this->mFilteredExifData[$prop] );
543 unset( $this->mFilteredExifData[$prop .
'Ref'] );
593 if ( !is_array( $in ) && sprintf(
'%d', $in ) == $in && $in >= 0 && $in <= 255 ) {
594 $this->
debug( $in, __FUNCTION__,
true );
598 $this->
debug( $in, __FUNCTION__,
false );
609 if ( is_array( $in ) ) {
613 if ( preg_match(
"/[^\x0a\x20-\x7e]/", $in ) ) {
614 $this->
debug( $in, __FUNCTION__,
'found a character not in our whitelist' );
619 if ( preg_match(
'/^\s*$/', $in ) ) {
620 $this->
debug( $in, __FUNCTION__,
'input consisted solely of whitespace' );
633 if ( !is_array( $in ) && sprintf(
'%d', $in ) == $in && $in >= 0 && $in <= 65536 ) {
634 $this->
debug( $in, __FUNCTION__,
true );
638 $this->
debug( $in, __FUNCTION__,
false );
649 if ( !is_array( $in ) && sprintf(
'%d', $in ) == $in && $in >= 0 && $in <= 4294967296 ) {
650 $this->
debug( $in, __FUNCTION__,
true );
654 $this->
debug( $in, __FUNCTION__,
false );
667 # Avoid division by zero
668 if ( !is_array( $in )
669 && preg_match(
'/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m )
671 return $this->
isLong( $m[1] ) && $this->
isLong( $m[2] );
673 $this->
debug( $in, __FUNCTION__,
'fed a non-fraction value' );
684 $this->
debug( $in, __FUNCTION__,
true );
694 if ( $this->
isLong( abs( $in ) ) ) {
695 $this->
debug( $in, __FUNCTION__,
true );
699 $this->
debug( $in, __FUNCTION__,
false );
712 # Avoid division by zero
713 if ( !is_array( $in ) &&
714 preg_match(
'/^(-?\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m )
718 $this->
debug( $in, __FUNCTION__,
'fed a non-fraction value' );
737 $etype = $this->mExifTags[
$section][$tag];
739 if ( is_array( $etype ) ) {
740 list( $etype, $ecount ) = $etype;
745 $count =
count( $val );
746 if ( $ecount != $count ) {
747 $this->
debug( $val, __FUNCTION__,
"Expected $ecount elements for $tag but got $count" );
752 foreach ( $val
as $v ) {
761 switch ( (
string)$etype ) {
765 return $this->
isByte( $val );
777 return $this->
isLong( $val );
803 $this->
debug( $val, __FUNCTION__,
"The tag '$tag' is unknown" );
820 $type = gettype( $in );
821 $class = ucfirst( __CLASS__ );
822 if ( is_array( $in ) ) {
823 $in = print_r( $in,
true );
826 if ( $action ===
true ) {
827 wfDebugLog( $this->log,
"$class::$fname: accepted: '$in' (type: $type)" );
828 } elseif ( $action ===
false ) {
829 wfDebugLog( $this->log,
"$class::$fname: rejected: '$in' (type: $type)" );
830 } elseif ( $action ===
null ) {
831 wfDebugLog( $this->log,
"$class::$fname: input was: '$in' (type: $type)" );
833 wfDebugLog( $this->log,
"$class::$fname: $action (type: $type; content: '$in')" );
847 $class = ucfirst( __CLASS__ );
849 wfDebugLog( $this->log,
"$class::$fname: begin processing: '{$this->basename}'" );
851 wfDebugLog( $this->log,
"$class::$fname: end processing: '{$this->basename}'" );