From: garvinhicking Date: Wed, 12 Apr 2006 14:41:19 +0000 (+0000) Subject: Parse, Display, Retrieve and Store EXIF, IPTC and XMP metadata. Phew. X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=46b9fbd06014b02ad0680abd633b6260875afc13;p=s9y.git Parse, Display, Retrieve and Store EXIF, IPTC and XMP metadata. Phew. --- diff --git a/docs/NEWS b/docs/NEWS index f1514ff..53632ce 100644 --- a/docs/NEWS +++ b/docs/NEWS @@ -10,8 +10,17 @@ Version 1.1-alpha4() * Make media manager able to store media properties (garvinhicking) TODO: - - Read/Parse EXIF metadata, ask which to import [config option?] - - Search/Filter for specific properties/keywords + - Video properties: Run length + - All properties: Creation date + - Prefill values from EXIF/IPTC/XMP metadata for Title, DPI, etc. + - Search/Filter for specific properties/keywords: + Sort by: Title, Filename, Creation date, Upload date, file type, author + Search by: Title, Creation date (From-To), Filetype, Comment(s), Keyword, Author + - Optionally increase filename (1, 2, 4, 5, ...) when file already exists and is not identical + - Store "real" filename when filename has been automatically inreased + - New option for image_Selector to save a specific sized version (?fid=XXX&targetSize=XX) + - Track referrers by image selector and store as property + - Restrict file upload by size/dimensions - Move/rename images/directories (browse serendipity_entries to fix up image paths [using ]. Also move ALL images of a directory, like when moving s9y installations. Put this into "Sync Thumbs" or "Manage Directories" panel. ? Create Smarty functions to access media properties by ID/filename diff --git a/include/functions_images.inc.php b/include/functions_images.inc.php index b4eae5f..2419195 100644 --- a/include/functions_images.inc.php +++ b/include/functions_images.inc.php @@ -1822,7 +1822,7 @@ function serendipity_getThumbNailPath($sRelativePath, $sName, $sExtension, $sThu * @return array Data about image * */ -function serendipity_getImageData($sRelativePath) { +function &serendipity_getImageData($sRelativePath) { global $serendipity; // First, peel off the file name from the path @@ -1898,7 +1898,13 @@ function serendipity_showPropertyForm(&$new_media, $keywordsPerBlock = 3, $is_ed $show[$idx] =& $media['internal']; $show[$idx]['image_id'] = $media['image_id']; + serendipity_prepareMedia($show[$idx]); + if (!is_array($props['base_metadata'])) { + $show[$idx]['metadata'] =& serendipity_getMetaData($show[$idx]['realfile'], $show[$idx]['header']); + } else { + $show[$idx]['metadata'] = $props['base_metadata']; + } foreach($dprops AS $prop) { $type = 'input'; @@ -1986,6 +1992,26 @@ function serendipity_parsePropertyForm() { serendipity_db_escape_string($val)); serendipity_db_query($q); } + + $s9y_img = $media['internal']; + $s9y_img['image_id'] = $media['image_id']; + serendipity_prepareMedia($s9y_img); + $s9y_img['metadata'] =& serendipity_getMetaData($s9y_img['realfile'], $s9y_img['header']); + serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}mediaproperties + WHERE mediaid = " . (int)$media['image_id'] . " + property_group = 'base_metadata'"); + foreach($s9y_img['metadata'] AS $maingroup => $items) { + foreach($items AS $key => $val) { + $q = sprintf("INSERT INTO {$serendipity['dbPrefix']}mediaproperties + (mediaid, property_group, property_subgroup, property, value) + VALUES (%d, 'base_metadata', '%s', '%s', '%s')", + $media['image_id'], + $maingroup, + serendipity_db_escape_string($key), + serendipity_db_escape_string($val)); + serendipity_db_query($q); + } + } } foreach($serendipity['POST']['mediaKeywords'] AS $id => $keywords) { @@ -2008,7 +2034,6 @@ function serendipity_parsePropertyForm() { 'image_id' => $serendipity['POST']['mediaProperties'][0]['image_id'], ); - return $array; } @@ -2031,6 +2056,8 @@ function &serendipity_fetchMediaProperties($id) { foreach($rows AS $row) { if (empty($row['property_subgroup'])) { $props[$row['property_group']][$row['property']] = $row['value']; + } else { + $props[$row['property_group']][$row['property_subgroup']][$row['property']] = $row['value']; } } } @@ -2089,9 +2116,10 @@ function serendipity_prepareMedia(&$file, $url = '') { if (!isset($file['imgsrc'])) { $file['imgsrc'] = $serendipity['uploadHTTPPath'] . $file['path'] . $file['name'] . (!empty($file['thumbnail_name']) ? '.' . $file['thumbnail_name'] : '') . '.' . $file['extension']; } - $file['dim'] = @getimagesize($img); + $file['dim'] = @getimagesize($img, $file['header']); $file['is_image'] = serendipity_isImage($file); $file['full_file'] = $serendipity['serendipityHTTPPath'] . $serendipity['uploadHTTPPath'] . $file['path'] . $file['name'] . '.'. $file['extension']; + $file['realfile'] = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $file['path'] . $file['name'] . '.'. $file['extension']; if ($full_perm || $serendipity['authorid'] == $file['authorid'] || $file['authorid'] == '0') { $file['is_editable'] = true; @@ -2142,6 +2170,7 @@ function serendipity_prepareMedia(&$file, $url = '') { * @param boolean Whether to show maintenance task items * @param int how many media items to display per row * @param boolean Enclose within a table cell? + * @param array Additional Smarty variables * @return boolean * */ @@ -2197,4 +2226,279 @@ function serendipity_showMedia(&$file, &$paths, $url = '', $manage = false, $lin } return true; -} \ No newline at end of file +} + +/** + * Convert a IPTC/EXIF/XMP item + * + * @param string The content + * @param string The type of the content + * @return string The converted content + * + */ +function serendipity_metaFieldConvert(&$item, $type) { + switch($type) { + case 'math': + $parts = explode('/', $item); + return ($parts[0] / $parts[1]); + break; + + case 'or': + if ($item == '1') { + return 'Landscape'; + } else { + return 'Portrait'; + } + + case 'date': + return strtotime($item); + break; + + case 'date2': + $parts = explode(':', $item); + return mktime($parts[3], $parts[4], $parts[5], $parts[1], $parts[2], $parts[0]); + break; + + case 'rdf': + if (preg_match('@]*>(.*)@i', $item, $ret)) { + return $ret[1]; + } + break; + + case 'text': + default: + return trim($item); + break; + } + + return ''; +} + +/** + * Get the RAW media header data (XMP) + * + * @param string Filename + * @return array The raw media header data + * + * Inspired, but rewritten, by "PHP JPEG Metadata Toolkit" from http://electronics.ozhiker.com. + * Code is GPL so sadly we couldn't bundle that GREAT library. + */ +function serendipity_getMediaRaw($filename) { + $abort = false; + + $f = @fopen($filename, 'rb'); + $ret = array(); + if (!$f) { + return $ret; + } + + $filedata = fread($f, 2); + + if ($filedata != "\xFF\xD8") { + fclose($f); + return $ret; + } + + $filedata = fread($f, 2); + + if ($filedata{0} != "\xFF") { + fclose($f); + return $ret; + } + + while (!$abort && !feof($f) && $filedata{1} != "\xD9") { + if ((ord($filedata{1}) < 0xD0) || (ord($filedata{1}) > 0xD7)) { + $ordret = fread($f, 2); + $ordstart = ftell($f); + $int = unpack('nsize', $ordret); + + if (ord($filedata{1}) == 225) { + $content = fread($f, $int['size'] - 2); + + if (substr($content, 0, 24) == 'http://ns.adobe.com/xap/') { + $ret[] = array( + 'ord' => ord($filedata{1}), + 'ordstart' => $ordstart, + 'int' => $int, + 'content' => $content + ); + } + } else { + fseek($f, $int['size'] - 2, SEEK_CUR); + } + } + + if ($filedata{1} == "\xDA") { + $abort = true; + } else { + $filedata = fread($f, 2); + if ($filedata{0} != "\xFF") { + fclose($f); + return $ret; + } + } + } + + fclose($f); + + return $ret; +} + +/** + * Get the IPTC/EXIF/XMP media metadata + * + * @param string Filename + * @return array The raw media header data + * + */ +function &serendipity_getMetaData($file, &$info) { + # Fields taken from: http://demo.imagefolio.com/demo/ImageFolio31_files/skins/cool_blue/images/iptc.html + static $IPTC_Fields = array( + '2#005' => 'ObjectName', + '2#025' => 'Keywords', + '2#026' => 'LocationCode', + '2#027' => 'LocationName', + '2#030' => 'ReleaseDate', + '2#035' => 'ReleaseTime', + '2#037' => 'ExpirationDate', + '2#038' => 'ExpirationTime', + '2#055' => 'DateCreated', + '2#060' => 'TimeCreated', + '2#062' => 'DigitalDateCreated', + '2#063' => 'DigitalTimeCreated', + '2#065' => 'Software', + '2#070' => 'SoftwareVersion', + '2#080' => 'Photographer', + '2#085' => 'Photographer Name', + '2#090' => 'PhotoLocation', + '2#092' => 'PhotoLocation2', + '2#095' => 'PhotoState', + '2#100' => 'PhotoCountryCode', + '2#101' => 'PhotoCountry', + '2#105' => 'Title', + '2#110' => 'Credits', + '2#115' => 'Source', + '2#116' => 'Copyright', + '2#118' => 'Contact', + '2#120' => 'Description', + '2#131' => 'Orientation', + '2#150' => 'AudioType', + '2#151' => 'AudioSamplingRate', + '2#152' => 'AudioSamplingResolution', + '2#153' => 'AudioDuration' + ); + + static $ExifFields = array( + 'IFD0' => array( + 'Make' => array('type' => 'text', 'name' => 'CameraMaker'), + 'Model' => array('type' => 'text', 'name' => 'CameraModel'), + 'Orientation' => array('type' => 'or', 'name' => 'Orientation'), + 'XResolution' => array('type' => 'math', 'name' => 'XResolution'), + 'YResolution' => array('type' => 'math', 'name' => 'YResolution'), + 'Software' => array('type' => 'text', 'name' => 'Software'), + 'DateTime' => array('type' => 'date2', 'name' => 'DateCreated'), + 'Artist' => array('type' => 'text', 'name' => 'Creator'), + ), + + 'EXIF' => array( + 'ExposureTime' => array('type' => 'math', 'name' => 'ExposureTime'), + 'ApertureValue' => array('type' => 'math', 'name' => 'ApertureValue'), + 'MaxApertureValue' => array('type' => 'math', 'name' => 'MaxApertureValue'), + 'ISOSpeedRatings' => array('type' => 'text', 'name' => 'ISOSpeedRatings'), + 'DateTimeOriginal' => array('type' => 'date2', 'name' => 'DateCreated'), + 'MeteringMode' => array('type' => 'text', 'name' => 'MeteringMode'), + 'FNumber' => array('type' => 'math', 'name' => 'FNumber'), + 'ExposureProgram' => array('type' => 'text', 'name' => 'ExposureProgram'), + 'FocalLength' => array('type' => 'math', 'name' => 'FocalLength'), + 'WhiteBalance' => array('type' => 'text', 'name' => 'WhiteBalance'), + 'DigitalZoomRatio' => array('type' => 'math', 'name' => 'DigitalZoomRatio'), + 'FocalLengthIn35mmFilm' => array('type' => 'text', 'name' => 'FocalLengthIn35mmFilm'), + 'Flash' => array('type' => 'text', 'name' => 'Flash'), + 'Fired' => array('type' => 'text', 'name' => 'FlashFired'), + 'RedEyeMode' => array('type' => 'text', 'name' => 'RedEyeMode'), + ) + ); + + static $xmpPatterns = array( + 'tiff:Orientation' => array('type' => 'or', 'name' => 'Orientation'), + 'tiff:XResolution' => array('type' => 'math', 'name' => 'XResolution'), + 'tiff:YResolution' => array('type' => 'math', 'name' => 'YResolution'), + 'tiff:Make' => array('type' => 'text', 'name' => 'CameraMaker'), + 'tiff:Model' => array('type' => 'text', 'name' => 'CameraModel'), + 'xap:ModifyDate' => array('type' => 'date', 'name' => 'DateModified'), + 'xap:CreatorTool' => array('type' => 'text', 'name' => 'Software'), + 'xap:CreateDate' => array('type' => 'date', 'name' => 'DateCreated'), + 'xap:MetadataDate' => array('type' => 'date', 'name' => 'DateMetadata'), + + 'exif:ExposureTime' => array('type' => 'math', 'name' => 'ExposureTime'), + 'exif:ApertureValue' => array('type' => 'math', 'name' => 'ApertureValue'), + 'exif:MaxApertureValue' => array('type' => 'math', 'name' => 'MaxApertureValue'), + 'exif:ISOSpeedRatings' => array('type' => 'text', 'name' => 'ISOSpeedRatings'), + 'exif:DateTimeOriginal' => array('type' => 'date', 'name' => 'DateCreated'), + 'exif:MeteringMode' => array('type' => 'text', 'name' => 'MeteringMode'), + 'exif:FNumber' => array('type' => 'math', 'name' => 'FNumber'), + 'exif:ExposureProgram' => array('type' => 'text', 'name' => 'ExposureProgram'), + 'exif:FocalLength' => array('type' => 'math', 'name' => 'FocalLength'), + 'exif:WhiteBalance' => array('type' => 'text', 'name' => 'WhiteBalance'), + 'exif:DigitalZoomRatio' => array('type' => 'math', 'name' => 'DigitalZoomRatio'), + 'exif:FocalLengthIn35mmFilm' => array('type' => 'text', 'name' => 'FocalLengthIn35mmFilm'), + 'exif:Fired' => array('type' => 'text', 'name' => 'FlashFired'), + 'exif:RedEyeMode' => array('type' => 'text', 'name' => 'RedEyeMode'), + + 'dc:title' => array('type' => 'rdf', 'name' => 'Title'), + 'dc:creator' => array('type' => 'rdf', 'name' => 'Creator'), + ); + + $ret = array(); + + if (!file_exists($file)) { + return $ret; + } + + if (function_exists('iptcparse') && is_array($info) && isset($info['APP13'])) { + $iptc = iptcparse($info['APP13']); + foreach($IPTC_Fields AS $field => $desc) { + if ($iptc[$field]) { + if (is_array($iptc[$field])) { + $ret['IPTC'][$desc] = trim(implode(';', $iptc[$field])); + } else { + $ret['IPTC'][$desc] = trim($iptc[$field]); + } + } + } + } + + if (function_exists('exif_read_data') && is_array($info)) { + $exif = @exif_read_data($file, 'FILE,COMPUTED,ANY_TAG,IFD0,COMMENT,EXIF', true, false); + if (is_array($exif)) { + foreach($ExifFields AS $Exifgroup => $ExifField) { + foreach($ExifField AS $ExifName => $ExifItem) { + if (!isset($exif[$Exifgroup][$ExifName])) { + continue; + } + $ret['EXIF'][$ExifItem['name']] = serendipity_metaFieldConvert($exif[$Exifgroup][$ExifName], $ExifItem['type']); + if ($ret['EXIF'][$item['name']] == $ret['IPTC'][$item['name']]) { + unset($ret['IPTC'][$item['name']]); + } + } + } + } + } + + $xmp = serendipity_getMediaRaw($file); + foreach($xmp AS $xmp_data) { + if (empty($xmp_data['content'])) { + continue; + } + foreach($xmpPatterns AS $lookup => $item) { + if (preg_match('@<' . $lookup . '>(.*)@', $xmp_data['content'], $match)) { + $ret['XMP'][$item['name']] = serendipity_metaFieldConvert($match[1], $item['type']); + if ($ret['EXIF'][$item['name']] == $ret['XMP'][$item['name']]) { + unset($ret['EXIF'][$item['name']]); + } + } + } + } + + return $ret; +} diff --git a/templates/default/admin/media_items.tpl b/templates/default/admin/media_items.tpl index f4f9162..2c0ed33 100644 --- a/templates/default/admin/media_items.tpl +++ b/templates/default/admin/media_items.tpl @@ -95,6 +95,23 @@ {/foreach} + +

EXIF/IPTC/XMP

+
+
+ {foreach from=$file.metadata key="meta_type" item="meta_data"} +

{$meta_type}

+
+ {foreach from=$meta_data key="meta_name" item="meta_value"} + + + + {/foreach} +
{$meta_name} + {$meta_value}
+ {/foreach} +
+
{/if} {if $media.enclose AND (($smarty.foreach.mediafiles.iteration % $media.lineBreak) == 0)}