From d01599c1c172934b0fdc79ee3249b46420424441 Mon Sep 17 00:00:00 2001 From: Thomas Bleher Date: Mon, 30 Aug 2010 03:35:18 +0800 Subject: [PATCH] Add IPTC module Taken from http://gallery.menalto.com/node/95875 (file http://gallery.menalto.com/files/iptc_0.zip) Author: gcog (I just imported it into Git unchanged) --- modules/iptc/controllers/admin_iptc.php | 84 ++++++++++ modules/iptc/helpers/iptc.php | 207 ++++++++++++++++++++++++ modules/iptc/helpers/iptc_block.php | 43 +++++ modules/iptc/helpers/iptc_event.php | 42 +++++ modules/iptc/helpers/iptc_installer.php | 46 ++++++ modules/iptc/helpers/iptc_task.php | 88 ++++++++++ modules/iptc/lib/functions.php | 97 +++++++++++ modules/iptc/models/iptc_key.php | 21 +++ modules/iptc/models/iptc_record.php | 21 +++ modules/iptc/module.info | 3 + modules/iptc/views/admin_iptc.html.php | 7 + modules/iptc/views/iptc_block.html.php | 9 ++ 12 files changed, 668 insertions(+) create mode 100644 modules/iptc/controllers/admin_iptc.php create mode 100644 modules/iptc/helpers/iptc.php create mode 100644 modules/iptc/helpers/iptc_block.php create mode 100644 modules/iptc/helpers/iptc_event.php create mode 100644 modules/iptc/helpers/iptc_installer.php create mode 100644 modules/iptc/helpers/iptc_task.php create mode 100644 modules/iptc/lib/functions.php create mode 100644 modules/iptc/models/iptc_key.php create mode 100644 modules/iptc/models/iptc_record.php create mode 100644 modules/iptc/module.info create mode 100644 modules/iptc/views/admin_iptc.html.php create mode 100644 modules/iptc/views/iptc_block.html.php diff --git a/modules/iptc/controllers/admin_iptc.php b/modules/iptc/controllers/admin_iptc.php new file mode 100644 index 00000000..dd803951 --- /dev/null +++ b/modules/iptc/controllers/admin_iptc.php @@ -0,0 +1,84 @@ +content = new View("admin_iptc.html"); + $view->content->iptc_form = $this->_get_admin_form(); + print $view; + } + + public function saveprefs() { + // Save user preferences to the database. + + // Prevent Cross Site Request Forgery + access::verify_csrf(); + + // Make sure the user filled out the form properly. + $form = $this->_get_admin_form(); + if ($form->validate()) { + Kohana_Log::add("error",print_r($form,1)); + + // Save settings to Gallery's database. + foreach (iptc::keys() as $keyword => $iptcvar) { + $checkbox = false; + for ($i = 0; $i < count($form->Global->$keyword); $i++) { + if ($form->Global->$keyword->value[$i] == $keyword) { + $checkbox = true; + } + } + module::set_var("iptc", "show_".$keyword, $checkbox); + } + // Display a success message and redirect back to the TagsMap admin page. + message::success(t("Your settings have been saved.")); + url::redirect("admin/iptc"); + } + + // Else show the page with errors + $view = new Admin_View("admin.html"); + $view->content = new View("admin_iptc.html"); + $view->content->iptc_form = $form; + print $view; + } + + private function _get_admin_form() { + // Make a new Form. + $form = new Forge("admin/iptc/saveprefs", "", "post", array("id" => "g-iptc-adminForm")); + + // Create group for display settings + $iptc_display_group = $form->group("Global") + ->label(t("Display Settings")); + + $show = t("Show"); + foreach (iptc::keys() as $keyword => $iptcvar) { + unset($checkbox); + $checkbox[$keyword] = array($show." \"".$iptcvar[1]."\" ?", module::get_var("iptc", "show_".$keyword)); + $iptc_display_group->checklist($keyword) + ->options($checkbox); + } + // Add a save button to the form. + $form->submit("SaveSettings")->value(t("Save")); + + // Return the newly generated form. + return $form; + } +} diff --git a/modules/iptc/helpers/iptc.php b/modules/iptc/helpers/iptc.php new file mode 100644 index 00000000..cca0f61f --- /dev/null +++ b/modules/iptc/helpers/iptc.php @@ -0,0 +1,207 @@ +is_photo() && $item->mime_type == "image/jpeg") { + $info = getJpegHeader($item->file_path()); + if ($info !== FALSE) { + $iptcBlock = getIptcBlock($info); + if ($iptcBlock !== FALSE) { + $iptc = iptcparse($iptcBlock); + } else { + $iptc = array(); + } + $xmp = getXmpDom($info); + + foreach (self::keys() as $keyword => $iptcvar) { + $iptc_key = $iptcvar[0]; + $xpath = $iptcvar[2]; + $value = null; + if ($xpath != null) { + $value = getXmpValue($xmp, $xpath); + } + if ($value == null) { + if (!empty($iptc[$iptc_key])) { + $value = implode(";", $iptc[$iptc_key]); + if (function_exists("mb_detect_encoding") && mb_detect_encoding($value) != "UTF-8") { + $value = utf8_encode($value); + } + } + } + if ($value != null) { + $keys[$keyword] = Input::clean($value); + } + } + } + } + + $record = ORM::factory("iptc_record")->where("item_id", "=", $item->id)->find(); + if (!$record->loaded()) { + $record->item_id = $item->id; + } + $record->data = serialize($keys); + $record->key_count = count($keys); + $record->dirty = 0; + $record->save(); + } + + static function get($item) { + $iptc = array(); + $record = ORM::factory("iptc_record") + ->where("item_id", "=", $item->id) + ->find(); + if (!$record->loaded()) { + return array(); + } + + $definitions = self::keys(); + $keys = unserialize($record->data); + foreach ($keys as $key => $value) { + if (module::get_var("iptc", "show_".$key) == 1) + $iptc[] = array("caption" => $definitions[$key][1], "value" => $value); + } + + return $iptc; + } + + + public static function keys() { + if (!isset(self::$iptc_keys)) { + self::$iptc_keys = array( + "ObjectName" => array("2#005", + t("IPTC Object Name"), + "/x:xmpmeta/rdf:RDF/rdf:Description/dc:title/rdf:Alt/rdf:li" ), + "EditStatus" => array("2#007", + t("IPTC Edit Status"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@mediapro:Status" ), + "Category" => array("2#015", + t("IPTC Category"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:Category" ), + "SupplementalCategories" => array("2#020", + t("IPTC Categories"), + "/x:xmpmeta/rdf:RDF/rdf:Description/photoshop:SupplementalCategories/rdf:Bag/rdf:li" ), + "FixtureIdentifier" => array("2#022", + t("IPTC Identifier"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@mediapro:Event" ), + "Keywords" => array("2#025", + t("IPTC Keywords"), + "/x:xmpmeta/rdf:RDF/rdf:Description/dc:subject/rdf:Bag/rdf:li" ), + "LocationCode" => array("2#026", + t("IPTC Location Code"), + null ), + "SpecialInstructions" => array("2#040", + t("IPTC Instructions"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:Instructions" ), + "DateCreated" => array("2#055", + t("IPTC Created Date"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:DateCreated" ), + "ByLine" => array("2#080", + t("IPTC Author"), + "/x:xmpmeta/rdf:RDF/rdf:Description/dc:creator/rdf:Seq/rdf:li" ), + "ByLineTitle" => array("2#085", + t("IPTC Author Title"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:AuthorsPosition" ), + "City" => array("2#090", + t("IPTC City"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:City" ), + "SubLocation" => array("2#092", + t("IPTC SubLocation"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@Iptc4xmpCore:Location | /x:xmpmeta/rdf:RDF/rdf:Description/@mediapro:Location" ), + "ProvinceState" => array("2#095", + t("IPTC Province State"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:State" ), + "CountryCode" => array("2#100", + t("IPTC Country Code"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@Iptc4xmpCore:CountryCode" ), + "CountryName" => array("2#101", + t("IPTC Country Name"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:Country" ), + "Transmission" => array("2#103", + t("IPTC Transmission,"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:TransmissionReference" ), + "HeadLine" => array("2#105", + t("IPTC HeadLine"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:Headline" ), + "Credit" => array("2#110", + t("IPTC Credit"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:Credit | /x:xmpmeta/rdf:RDF/rdf:Description/photoshop:Credit" ), + "Source" => array("2#115", + t("IPTC Source"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:Source" ), + "Copyright" => array("2#116", + t("IPTC Copyright"), + "/x:xmpmeta/rdf:RDF/rdf:Description/dc:rights/rdf:Alt/rdf:li" ), + "Contacts" => array("2#118", + t("IPTC Contacts"), + "/x:xmpmeta/rdf:RDF/rdf:Description/mediapro:People/rdf:Bag/rdf:li" ), + "Caption" => array("2#120", + t("IPTC Caption"), + "/x:xmpmeta/rdf:RDF/rdf:Description/dc:description/rdf:Alt/rdf:li" ), + "Redactor" => array("2#122", + t("IPTC Redactor"), + "/x:xmpmeta/rdf:RDF/rdf:Description/@photoshop:CaptionWriter" ) + ); + } + return self::$iptc_keys; + } + + + static function stats() { + $missing_iptc = db::build() + ->select("items.id") + ->from("items") + ->join("iptc_records", "items.id", "iptc_records.item_id", "left") + ->where("type", "=", "photo") + ->and_open() + ->where("iptc_records.item_id", "IS", null) + ->or_where("iptc_records.dirty", "=", 1) + ->close() + ->execute() + ->count(); + + $total_items = ORM::factory("item")->where("type", "=", "photo")->count_all(); + if (!$total_items) { + return array(0, 0, 0); + } + return array($missing_iptc, $total_items, + round(100 * (($total_items - $missing_iptc) / $total_items))); + } + + static function check_index() { + list ($remaining) = iptc::stats(); + if ($remaining) { + site_status::warning( + t('Your Iptc index needs to be updated. Fix this now', + array("url" => html::mark_clean(url::site("admin/maintenance/start/iptc_task::update_index?csrf=__CSRF__")))), + "iptc_index_out_of_date"); + } + } +} diff --git a/modules/iptc/helpers/iptc_block.php b/modules/iptc/helpers/iptc_block.php new file mode 100644 index 00000000..1a6ed955 --- /dev/null +++ b/modules/iptc/helpers/iptc_block.php @@ -0,0 +1,43 @@ + t("IPTC info")); + } + + static function get($block_id, $theme) { + $block = ""; + switch ($block_id) { + case "iptc": + if ($theme->item()) { + $details = iptc::get($theme->item()); + if (count($details) > 0) { + $block = new Block(); + $block->css_id = "g-metadata"; + $block->title = t("IPTC info"); + $block->content = new View("iptc_block.html"); + $block->content->details = $details; + } + } + break; + } + return $block; + } +} \ No newline at end of file diff --git a/modules/iptc/helpers/iptc_event.php b/modules/iptc/helpers/iptc_event.php new file mode 100644 index 00000000..c7b4a6cc --- /dev/null +++ b/modules/iptc/helpers/iptc_event.php @@ -0,0 +1,42 @@ +is_photo()) { + iptc::extract($item); + } + } + + static function item_deleted($item) { + db::build() + ->delete("iptc_records") + ->where("item_id", "=", $item->id) + ->execute(); + } + + static function admin_menu($menu, $theme) { + // Add a link to the admin page to the Settings menu. + $menu->get("settings_menu") + ->append(Menu::factory("link") + ->id("iptc") + ->label(t("IPTC Settings")) + ->url(url::site("admin/iptc"))); + } +} diff --git a/modules/iptc/helpers/iptc_installer.php b/modules/iptc/helpers/iptc_installer.php new file mode 100644 index 00000000..11a17a56 --- /dev/null +++ b/modules/iptc/helpers/iptc_installer.php @@ -0,0 +1,46 @@ +query("CREATE TABLE IF NOT EXISTS {iptc_records} ( + `id` int(9) NOT NULL auto_increment, + `item_id` INTEGER(9) NOT NULL, + `key_count` INTEGER(9) default 0, + `data` TEXT, + `dirty` BOOLEAN default 1, + PRIMARY KEY (`id`), + KEY(`item_id`)) + DEFAULT CHARSET=utf8;"); + module::set_version("iptc", 1); + } + + static function activate() { + iptc::check_index(); + } + + static function deactivate() { + site_status::clear("iptc_index_out_of_date"); + } + + static function uninstall() { + Database::instance()->query("DROP TABLE IF EXISTS {iptc_records};"); + } +} diff --git a/modules/iptc/helpers/iptc_task.php b/modules/iptc/helpers/iptc_task.php new file mode 100644 index 00000000..d4715bff --- /dev/null +++ b/modules/iptc/helpers/iptc_task.php @@ -0,0 +1,88 @@ +delete("iptc_records") + ->where("item_id", "NOT IN", + db::build()->select("id")->from("items")->where("type", "=", "photo")) + ->execute(); + + list ($remaining, $total, $percent) = iptc::stats(); + return array(Task_Definition::factory() + ->callback("iptc_task::update_index") + ->name(t("Extract Iptc data")) + ->description($remaining + ? t2("1 photo needs to be scanned", + "%count (%percent%) of your photos need to be scanned", + $remaining, array("percent" => (100 - $percent))) + : t("IPTC data is up-to-date")) + ->severity($remaining ? log::WARNING : log::SUCCESS)); + } + + static function update_index($task) { + try { + $completed = $task->get("completed", 0); + + $start = microtime(true); + foreach (ORM::factory("item") + ->join("iptc_records", "items.id", "iptc_records.item_id", "left") + ->where("type", "=", "photo") + ->and_open() + ->where("iptc_records.item_id", "IS", null) + ->or_where("iptc_records.dirty", "=", 1) + ->close() + ->find_all() as $item) { + // The query above can take a long time, so start the timer after its done + // to give ourselves a little time to actually process rows. + if (!isset($start)) { + $start = microtime(true); + } + + iptc::extract($item); + $completed++; + + if (microtime(true) - $start > 1.5) { + break; + } + } + + list ($remaining, $total, $percent) = iptc::stats(); + $task->set("completed", $completed); + if ($remaining == 0 || !($remaining + $completed)) { + $task->done = true; + $task->state = "success"; + site_status::clear("iptc_index_out_of_date"); + $task->percent_complete = 100; + } else { + $task->percent_complete = round(100 * $completed / ($remaining + $completed)); + } + $task->status = t2("one record updated, index is %percent% up-to-date", + "%count records updated, index is %percent% up-to-date", + $completed, array("percent" => $percent)); + } catch (Exception $e) { + $task->done = true; + $task->state = "error"; + $task->status = $e->getMessage(); + $task->log((string)$e); + } + } +} diff --git a/modules/iptc/lib/functions.php b/modules/iptc/lib/functions.php new file mode 100644 index 00000000..91ca129f --- /dev/null +++ b/modules/iptc/lib/functions.php @@ -0,0 +1,97 @@ + 0xD7) { + $size = fread($file, 2); + if ($size === FALSE) { + fclose($file); + return $result; + } + $sizeOfSegment = unpack("nV", $size); + $data = fread($file, $sizeOfSegment['V']-2); + if ($data === FALSE) { + fclose($file); + return $result; + } + if ($result === FALSE) + unset($result); + $result[] = array("type" => $typeOfSegment, "data" => $data); // Multiple segments can have the same type like Exif and XMP + } + } while (!feof($file)); + fclose($file); + return $result; +} + + +function getIptcBlock($jpegHeader) +{ + for ($i = 0; $i < count($jpegHeader); $i++) { + if ($jpegHeader[$i]['type'] == 0xED) { + if (strncmp($jpegHeader[$i]['data'], "Photoshop 3.0\x00", 14) == 0) { + return $jpegHeader[$i]['data']; + } + } + } + return FALSE; +} + + +function getXmpDom($jpegHeader) +{ + for ($i = 0; $i < count($jpegHeader); $i++) { + if ($jpegHeader[$i]['type'] == 0xE1) { + if (strncmp($jpegHeader[$i]['data'], "http://ns.adobe.com/xap/1.0/\x00", 29) == 0) { + $xmlstr = substr($jpegHeader[$i]['data'], 29); + $doc = new DOMDocument(); + $doc->loadXML($xmlstr); + return $doc; + } + } + } + return FALSE; +} + + +function getXmpValue($dom, $xpathQuery) +{ + if ($dom === FALSE) + return null; + $xpath = new DOMXPath($dom); + $xpath->registerNamespace('rdf', "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); + $xpath->registerNamespace('photoshop', "http://ns.adobe.com/photoshop/1.0/"); + $xpath->registerNamespace('Iptc4xmpCore', "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"); + $xpath->registerNamespace('dc', "http://purl.org/dc/elements/1.1/"); + $xpath->registerNamespace('mediapro', "http://ns.iview-multimedia.com/mediapro/1.0/"); + $nodeList = $xpath->query($xpathQuery); + $result = ""; + foreach ($nodeList as $node) { + if (!empty($result)) + $result .= ';'; + $result .= $node->nodeValue; + } + return $result; +} + diff --git a/modules/iptc/models/iptc_key.php b/modules/iptc/models/iptc_key.php new file mode 100644 index 00000000..fadcb37b --- /dev/null +++ b/modules/iptc/models/iptc_key.php @@ -0,0 +1,21 @@ + +
+

+
+ +
+
diff --git a/modules/iptc/views/iptc_block.html.php b/modules/iptc/views/iptc_block.html.php new file mode 100644 index 00000000..c446935a --- /dev/null +++ b/modules/iptc/views/iptc_block.html.php @@ -0,0 +1,9 @@ + +