* $image = ezcImageAnalyzer( '/var/cache/images/toby.jpg' ); * $mime = $image->mime; * if ( $mime == 'image/tiff' || $mime == 'image/jpeg' ) * { * echo 'Photo taken on '.date( 'Y/m/d, H:i', $image->date ).".\n"; * } * elseif ( $mime !== false ) * { * echo "Format was detected as {$mime}.\n"; * } * else * { * echo "Unknown photo format.\n"; * } * * * @package ImageAnalysis */ class ezcImageAnalyzer { /** * Image is built with a palette and consists of indexed values per pixel. */ const MODE_INDEXED = 1; /** * Image consists of RGB value per pixel. */ const MODE_TRUECOLOR = 2; /** * No parts of image is transparent. */ const TRANSPARENCY_OPAQUE = 1; /* * Selected palette entries are completely see-through. */ const TRANSPARENCY_TRANSPARENT = 2; /** * Transparency determined pixel per pixel with a fuzzy value. */ const TRANSPARENCY_TRANSLUCENT = 3; /** * The path of the file to analyze. * * @var string */ protected $filePath; /** * EXIF information retrieved from image. * This will only be filled in for images which supports EXIF entries, * currently they are: * - image/jpeg * - image/tiff * * @link http://php.net/manual/en/function.exif-read-data.php * * @var array */ protected $exif; /** * Width of image in pixels. * * @var int */ protected $width; /** * Height of image in pixels. * * @var int */ protected $height; /** * Size of image file in bytes. * * @var int */ protected $size; /** * The image mode. * Can be one of: * - self::MODE_INDEXED - Image is built with a palette and consists of * indexed values per pixel. * - self::MODE_TRUECOLOR - Image consists of RGB value per pixel. * * @var int */ protected $mode; /** * Type of transperance in image. * Can be one of: * - self::TRANSPARENCY_OPAQUE - No parts of image is transparent. * - self::TRANSPARENCY_TRANSPARENT - Selected palette entries are * completely see-through. * - self::TRANSPARENCY_TRANSLUCENT - Transparency determined pixel per * pixel with a fuzzy value. * * @var int */ protected $transparencyType; /** * Does the image have colors? * * @var bool */ protected $isColor; /** * Number of colors in image. * * @var int */ protected $colorCount; /** * First inline comment for the image. * * @var string */ protected $comment; /** * List of inline comments for the image. * * @var array */ protected $commentList; /** * Copyright text for the image. * * @var string */ protected $copyright; /** * The date when the picture was taken as UNIX timestamp. * * @var int */ protected $date; /** * Does the image have a thumbnail? * * @var bool */ protected $hasThumbnail; /** * Is the image animated? * * @var bool */ protected $isAnimated; /** * Detected MIME type for the image. * * @var string */ protected $mime; /** * Determines whether the image file has been analyzed or not. * This is used internally. * * @var bool */ protected $isAnalyzed; /** * Create an image analyzer for the specified file. * * @param string $file The file to analyze. * * @throws ezcImageAnalyzerException If image could not be analyzed. */ public function __construct( $file ) { if ( !is_file( $file ) ) { throw new ezcImageAnalyzerException( "File does not exist <{$file}>.", ezcImageAnalyzerException::FILE_NOT_EXISTS ); } if ( !is_readable( $file ) ) { throw new ezcImageAnalyzerException( "File is not readable <{$file}.", ezcImageAnalyzerException::FILE_NOT_READABLE ); } $this->filePath = $file; $this->isAnalyzed = false; $this->analyzeType(); } /** * Sets the property $name to $value. * * @throws ezcBasePropertyNotFoundException if the property does not exist. * @throws ezcBasePropertyReadOnlyException if the property cannot be modified. * @param string $name * @param mixed $value * @return void */ public function __set( $name, $value ) { switch ( $name ) { case 'mime': case 'exif': case 'width': case 'height': case 'size': case 'mode': case 'transparencyType': case 'isColor': case 'colorCount': case 'comment': case 'commentList': case 'copyright': case 'date': case 'hasThumbnail': case 'isAnimated': throw new ezcBasePropertyReadOnlyException( $name ); default: throw new ezcBasePropertyNotFoundException( $name ); } } /** * Returns the property $name. * * @throws ezcBasePropertyNotFoundException if the property does not exist. * * @param string $name Name of the property to access. * @return mixed Value of the desired property. */ public function __get( $name ) { switch ( $name ) { case 'mime': return $this->mime; case 'exif': case 'width': case 'height': case 'size': case 'mode': case 'transparencyType': case 'isColor': case 'colorCount': case 'comment': case 'commentList': case 'copyright': case 'date': case 'hasThumbnail': case 'isAnimated': if ( !$this->isAnalyzed ) { $this->analyzeImage(); } return $this->$name; default: throw new ezcBasePropertyNotFoundException( $name ); } } /** * Checks if the property $name exist and returns the result. * * @param string $name * @return bool */ public function __isset( $name ) { switch ( $name ) { case 'mime': case 'exif': case 'width': case 'height': case 'size': case 'mode': case 'transparencyType': case 'isColor': case 'colorCount': case 'comment': case 'commentList': case 'copyright': case 'date': case 'hasThumbnail': case 'isAnimated': return true; default: return false; } } /** * Analyzes the image type. * This method analyzes image data to determine the MIME type and * saves these info in self::$mime. * * @throws ezcImageAnalyzerException if the type could not be determined. * * @return void */ private function analyzeType() { $data = getimagesize( $this->filePath ); if ( $data === false ) { throw new ezcImageAnalyzerException( "Could not determine MIME type of file <{$this->filePath}>.", ezcImageAnalyzerException::FILE_NOT_PROCESSABLE ); } $imagetype = $data[2]; switch ( $imagetype ) { case IMAGETYPE_GIF: $this->mime = 'image/gif'; break; case IMAGETYPE_JPEG: $this->mime = 'image/jpeg'; break; case IMAGETYPE_PNG: $this->mime = 'image/png'; break; case IMAGETYPE_SWF: $this->mime = 'application/swf'; break; case IMAGETYPE_PSD: $this->mime = 'image/psd'; break; case IMAGETYPE_BMP: $this->mime = 'image/bmp'; break; case IMAGETYPE_TIFF_MM: case IMAGETYPE_TIFF_II: $this->mime = 'image/tiff'; break; case IMAGETYPE_JPC: $this->mime = 'image/jpc'; break; case IMAGETYPE_JP2: $this->mime = 'image/jp2'; break; case IMAGETYPE_JPX: $this->mime = 'image/jpx'; break; case IMAGETYPE_JB2: $this->mime = 'image/x-jb2'; break; case IMAGETYPE_SWC: $this->mime = 'image/x-swc'; break; case IMAGETYPE_IFF: $this->mime = 'video/x-anim'; break; case IMAGETYPE_WBMP: $this->mime = 'image/vnd.wap.wbmp'; break; case IMAGETYPE_XBM: default: throw new ezcImageAnalyzerException( "Could not determine MIME type of file <{$this->filePath}>.", ezcImageAnalyzerException::FILE_NOT_PROCESSABLE ); break; } } /** * Analyze the image for detailed information. * Depending on the file type it calls: * - analyzeExif() for image/jpeg and image/tiff. * - analyzeGif() for image/gif * - analyzeGeneric() for all other formats * * This sets self::$isAnalyzed to true when done. * * @return void */ private function analyzeImage() { if ( ( $this->mime === 'image/jpeg' || $this->mime === 'image/tiff' ) ) { $this->analyzeExif(); } elseif ( $this->mime === 'image/gif' ) { $this->analyzeGif(); } else { $this->analyzeGeneric(); } $this->isAnalyzed = true; } /** * Analyze image file format for generic data entries. * The image file is analyzed by calling getimagesize and placing the * result in self::width and self::height. * * @return void */ private function analyzeGeneric() { $data = getimagesize( $this->filePath, $info ); $this->width = $data[0]; $this->height = $data[1]; $this->size = filesize( $this->filePath ); } /** * Analyze EXIF enabled file format for EXIF data entries. * The image file is analyzed by calling exif_read_data and placing the * result in self::exif. In addition it fills in extra properties from * the EXIF data for easy and uniform access. */ private function analyzeExif() { $this->exif = exif_read_data( $this->filePath, "COMPUTED,FILE", true, false ); // Section "COMPUTED" if ( isset( $this->exif['COMPUTED']['Width'] ) && isset( $this->exif['COMPUTED']['Height'] ) ) { $this->width = $this->exif['COMPUTED']['Width']; $this->height = $this->exif['COMPUTED']['Height']; } if ( isset( $this->exif['COMPUTED']['IsColor'] ) ) { $this->isColor = $this->exif['COMPUTED']['IsColor'] == 1; } if ( isset( $this->exif['COMPUTED']['UserComment'] ) ) { $this->comment = $this->exif['COMPUTED']['UserComment']; $this->commentList = array( $this->comment ); } if ( isset( $this->exif['COMPUTED']['Copyright'] ) ) { $this->copyright = $this->exif['COMPUTED']['Copyright']; } // Section THUMBNAIL $this->hasThumbnail = isset( $this->exif['THUMBNAIL'] ); // Section "FILE" if ( isset( $this->exif['FILE']['FileSize'] ) ) { $this->size = $this->exif['FILE']['FileSize']; } if ( isset( $this->exif['FILE']['FileDateTime'] ) ) { $this->date = $this->exif['FILE']['FileDateTime']; } // EXIF based image are never animated. $this->isAnimated = false; } /** * Analyze GIF files for detailed information. * The GIF file is analyzed by scanning for frame entries, if more than one * is found it is assumed to be animated. * It also extracts other information such as image width and height, color * count, image mode, transparency type and comments. * * @return void */ private function analyzeGif() { if ( ( $fp = fopen( $this->filePath, 'rb' ) ) === false ) { throw new ezcImageAnalyzerException( "Could not open file <{$this->filePath}> for reading.", ezcImageAnalyzerException::FILE_NOT_READABLE ); } // Read GIF header $magic = fread( $fp, 6 ); $offset = 6; if ( $magic != 'GIF87a' && $magic != 'GIF89a' ) { throw new ezcImageAnalyzerException( "Not a valid GIF image file <{$this->filePath}>.", ezcImageAnalyzerException::FILE_NOT_PROCESSABLE ); } $info = array(); $version = substr( $magic, 3 ); $frames = 0; // Gifs are always indexed $this->mode = self::MODE_INDEXED; $this->commentList = array(); $this->transparencyType = self::TRANSPARENCY_OPAQUE; // Read Logical Screen Descriptor $data = unpack( "v1width/v1height/C1bitfield/C1index/C1ration", fread( $fp, 7 ) ); $offset += 7; $lsdFields = $data['bitfield']; $globalColorCount = 0; $globalColorTableSize = 0; if ( $lsdFields >> 7 ) { // Extract 3 bits for color count $globalColorCount = ( 1 << ( ( $lsdFields & 0x07 ) + 1) ); // Each color entry is RGB ie. 3 bytes $globalColorTableSize = $globalColorCount * 3; } $this->colorCount = $globalColorCount; $this->width = $data['width']; $this->height = $data['height']; if ( $globalColorTableSize ) { // Skip the color table, we don't need the data fseek( $fp, $globalColorTableSize, SEEK_CUR ); $offset += $globalColorTableSize; } $done = false; // Iterate over all blocks and extract information while ( !$done ) { $data = fread( $fp, 1 ); $offset += 1; $blockType = ord( $data[0] ); if ( $blockType == 0x21 ) // Extension Introducer { $data .= fread( $fp, 1 ); $offset += 1; $extensionLabel = ord( $data[1] ); if ( $extensionLabel == 0xf9 ) // Graphical Control Extension { $data = unpack( "C1blocksize/C1bitfield/v1delay/C1index/C1term", fread( $fp, 5 + 1 ) ); $gceFlags = $data['bitfield'];//ord( $data[1] ); // $animationTimer is currently not in use. /* $animationTimer = $data['delay']; */ // Check bit 0 if ( $gceFlags & 0x01 ) { $this->transparencyType = self::TRANSPARENCY_TRANSPARENT; } $offset += 5 + 1; } else if ( $extensionLabel == 0xff ) // Application Extension { $data = fread( $fp, 12 ); $offset += 12; $dataBlockDone = false; while ( !$dataBlockDone ) { $data = unpack( "C1blocksize", fread( $fp, 1 ) ); $offset += 1; $blockBytes = $data['blocksize']; if ( $blockBytes ) { // Skip application data, we don't need the data fseek( $fp, $blockBytes, SEEK_CUR ); $offset += $blockBytes; } else { $dataBlockDone = true; } } } else if ( $extensionLabel == 0xfe ) // Comment Extension { $commentBlockDone = false; $comment = false; while ( !$commentBlockDone ) { $data = unpack( "C1blocksize", fread( $fp, 1 ) ); $offset += 1; $blockBytes = $data['blocksize']; if ( $blockBytes ) { // Append current block to comment $data = fread( $fp, $blockBytes ); $comment .= $data; $offset += $blockBytes; } else { $commentBlockDone = true; } } if ( $comment ) { if ( $this->comment === null ) { $this->comment = $comment; } $this->commentList[] = $comment; } } else { throw new ezcImageAnalyzerException( "Invalid extension label 0x" . hexdec( $extensionLabel ) . " in GIF image file <{$this->filePath}>.", ezcImageAnalyzerException::FILE_NOT_PROCESSABLE ); } } else if ( $blockType == 0x2c ) // Image Descriptor { ++$frames; $data .= fread( $fp, 9 ); $data = unpack( "C1separator/v1leftpos/v1toppos/v1width/v1height/C1bitfield", $data ); $localColorTableSize = 0; $localColorCount = 0; $idFields = $data['bitfield']; if ( $idFields >> 7 ) // Local Color Table { // Extract 3 bits for color count $localColorCount = ( 1 << ( ( $idFields & 0x07 ) + 1) ); // Each color entry is RGB ie. 3 bytes $localColorTableSize = $localColorCount * 3; } if ( $localColorCount > $globalColorCount ) { $this->colorCount = $localColorCount; } if ( $localColorTableSize ) { // Skip the color table, we don't need the data fseek( $fp, $localColorTableSize, SEEK_CUR ); $offset += $localColorTableSize; } $lzwCodeSize = fread( $fp, 1 ); // LZW Minimum Code Size, currently unused $offset += 1; $dataBlockDone = false; while ( !$dataBlockDone ) { $data = unpack( "C1blocksize", fread( $fp, 1 ) ); $offset += 1; $blockBytes = $data['blocksize']; if ( $blockBytes ) { // Skip image data, we don't need the data fseek( $fp, $blockBytes, SEEK_CUR ); $offset += $blockBytes; } else { $dataBlockDone = true; } } } else if ( $blockType == 0x3b ) // Trailer, end of stream { $done = true; } else { throw new ezcImageAnalyzerException( "Invalid block type 0x" . hexdec( $blockType ) . " in GIF image file <{$this->filePath}>.", ezcImageAnalyzerException::FILE_NOT_PROCESSABLE ); } if ( feof( $fp ) ) { break; } } $this->isAnimated = $frames > 1; $this->size = filesize( $this->filePath ); } } ?>