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
128 'Orientation' =>
self::SHORT, # Orientation of image #p24
129 'SamplesPerPixel' =>
self::SHORT, # Number of components
130 'PlanarConfiguration' =>
self::SHORT, # Image data arrangement #p24
131 'YCbCrSubSampling' => [
self::SHORT, 2 ], # Subsampling ratio of Y to C #p24
132 'YCbCrPositioning' =>
self::SHORT, # Y and C positioning #p24-25
133 'XResolution' =>
self::RATIONAL, # Image resolution in width direction
134 'YResolution' =>
self::RATIONAL, # Image resolution in height direction
135 'ResolutionUnit' =>
self::SHORT, # Unit of X and Y resolution #(p26)
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
153 'DateTime' => self::ASCII, #
File change date and time
154 'ImageDescription' => self::ASCII, # Image title
155 'Make' => self::ASCII, # Image input equipment manufacturer
156 'Model' => self::ASCII, # Image input equipment model
157 'Software' => self::ASCII, # Software used
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
173 'ComponentsConfiguration' =>
self::UNDEFINED, # Meaning of each component #p33
174 'CompressedBitsPerPixel' =>
self::RATIONAL, # Image compression mode
178 # Tags relating to related user information
182 # Tags relating to related file information
183 'RelatedSoundFile' =>
self::ASCII, # Related audio file
185 # Tags relating to date and time
186 'DateTimeOriginal' =>
self::ASCII, # Date and time of original data generation #p36
187 'DateTimeDigitized' =>
self::ASCII, # Date and time of original data generation
189 'SubSecTimeOriginal' =>
self::ASCII, # DateTimeOriginal subseconds
190 'SubSecTimeDigitized' =>
self::ASCII, # DateTimeDigitized subseconds
192 # Tags relating to picture-taking conditions (p31)
195 'ExposureProgram' =>
self::SHORT, # Exposure Program #p38
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.
206 'MeteringMode' =>
self::SHORT, # Metering mode #p40
207 'LightSource' =>
self::SHORT, # Light source #p40-41
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
221 'CFAPattern' =>
self::IGNORE, # CFA pattern. not supported atm.
222 'CustomRendered' =>
self::SHORT, # Custom image processing #p48
223 'ExposureMode' =>
self::SHORT, # Exposure mode #p48
224 'WhiteBalance' =>
self::SHORT, # White Balance #p49
226 'FocalLengthIn35mmFilm' =>
self::SHORT, # Focal length in 35 mm film
227 'SceneCaptureType' =>
self::SHORT, # Scene capture type #p49
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)
254 'GPSSatellites' =>
self::ASCII, # Satellites used
for measurement
256 'GPSMeasureMode' =>
self::ASCII, # Measurement mode #p54-55
260 'GPSTrackRef' =>
self::ASCII, # Reference
for direction of movement #p55
262 'GPSImgDirectionRef' =>
self::ASCII, # Reference
for direction of image #p56
264 'GPSMapDatum' =>
self::ASCII, # Geodetic survey data used
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
268 'GPSDestLongitude' => [
self::RATIONAL, 3 ], # Longitude of destination
269 'GPSDestBearingRef' =>
self::ASCII, # Reference
for bearing of destination #p57
271 'GPSDestDistanceRef' =>
self::ASCII, # Reference
for distance to destination #p57-58
273 'GPSProcessingMethod' =>
self::UNDEFINED, # Name of GPS processing method
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';
294 if ( function_exists(
'exif_read_data' ) ) {
295 Wikimedia\suppressWarnings();
296 $data = exif_read_data( $this->file, 0,
true );
297 Wikimedia\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];
335 $value = $this->mRawExifData[$section][$tag];
336 if ( !$this->
validate( $section, $tag, $value ) ) {
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 "JIS\x00\x00\x00\x00\x00":
459 $charset =
"Shift-JIS";
469 Wikimedia\suppressWarnings();
470 $val = iconv( $charset,
'UTF-8//IGNORE', $val );
471 Wikimedia\restoreWarnings();
475 UtfNormal\Validator::quickIsNFCVerify( $valCopy );
476 if ( $valCopy !== $val ) {
477 Wikimedia\suppressWarnings();
478 $val = iconv(
'Windows-1252',
'UTF-8//IGNORE', $val );
479 Wikimedia\restoreWarnings();
485 if ( strlen( $val ) === 0 ) {
487 $this->
debug( $this->mFilteredExifData[$prop], __FUNCTION__,
"$prop: Is only whitespace" );
488 unset( $this->mFilteredExifData[$prop] );
494 $this->mFilteredExifData[$prop] = $val;
505 if ( isset( $this->mFilteredExifData[$prop] ) ) {
506 $this->mFilteredExifData[$prop] = ord( $this->mFilteredExifData[$prop] );
516 $loc =& $this->mFilteredExifData[$prop];
517 $dir =& $this->mFilteredExifData[$prop .
'Ref'];
520 if ( isset( $loc ) && isset( $dir )
521 && ( $dir ===
'N' || $dir ===
'S' || $dir ===
'E' || $dir ===
'W' )
523 list( $num, $denom ) = explode(
'/', $loc[0] );
524 $res = $num / $denom;
525 list( $num, $denom ) = explode(
'/', $loc[1] );
526 $res += ( $num / $denom ) * ( 1 / 60 );
527 list( $num, $denom ) = explode(
'/', $loc[2] );
528 $res += ( $num / $denom ) * ( 1 / 3600 );
530 if ( $dir ===
'S' || $dir ===
'W' ) {
537 if (
$res !==
false ) {
538 $this->mFilteredExifData[$prop] =
$res;
539 unset( $this->mFilteredExifData[$prop .
'Ref'] );
541 unset( $this->mFilteredExifData[$prop] );
542 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' );
735 private function validate( $section, $tag, $val, $recursive =
false ) {
737 $etype = $this->mExifTags[$section][$tag];
739 if ( is_array( $etype ) ) {
740 list( $etype, $ecount ) = $etype;
747 if ( is_array( $val ) ) {
748 $count = count( $val );
749 if ( $ecount != $count ) {
750 $this->
debug( $val, __FUNCTION__,
"Expected $ecount elements for $tag but got $count" );
756 foreach ( $val as $v ) {
757 if ( !$this->
validate( $section, $tag, $v,
true ) ) {
765 switch ( (
string)$etype ) {
769 return $this->
isByte( $val );
781 return $this->
isLong( $val );
807 $this->
debug( $val, __FUNCTION__,
"The tag '$tag' is unknown" );
820 private function debug( $in, $fname, $action =
null ) {
824 $type = gettype( $in );
825 $class = ucfirst( __CLASS__ );
826 if ( is_array( $in ) ) {
827 $in = print_r( $in,
true );
830 if ( $action ===
true ) {
831 wfDebugLog( $this->log,
"$class::$fname: accepted: '$in' (type: $type)" );
832 } elseif ( $action ===
false ) {
833 wfDebugLog( $this->log,
"$class::$fname: rejected: '$in' (type: $type)" );
834 } elseif ( $action ===
null ) {
835 wfDebugLog( $this->log,
"$class::$fname: input was: '$in' (type: $type)" );
837 wfDebugLog( $this->log,
"$class::$fname: $action (type: $type; content: '$in')" );
851 $class = ucfirst( __CLASS__ );
853 wfDebugLog( $this->log,
"$class::$fname: begin processing: '{$this->basename}'" );
855 wfDebugLog( $this->log,
"$class::$fname: end processing: '{$this->basename}'" );