diff --git a/CHANGELOG.md b/CHANGELOG.md index b790bc20c..79f1ae1ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ ## mm/dd/2017 1. [](#new) + * Added support for a single array field in the forms + * Added EXIF support with automatic generation of Page Media metafiles + * Added Twig function to get EXIF data on any image file * Added `Pages::baseUrl()`, `Pages::homeUrl()` and `Pages::url()` functions * Added `base32_encode`, `base32_decode`, `base64_encode`, `base64_decode` Twig filters * Added `Debugger::getCaller()` to figure out where the method was called from @@ -23,6 +26,7 @@ * Groups selection pre-filled in user form * Improve error handling in `Folder::move()` * Added extra parameter for `Twig::processSite()` to include custom context + * Updated RocketTheme Toolbox vendor library 1. [](#bugfix) * Fix to force route/redirect matching from the start of the route by default [#1446](https://github.com/getgrav/grav/issues/1446) * Edit check for valid slug [#1459](https://github.com/getgrav/grav/issues/1459) diff --git a/composer.json b/composer.json index e8151b18d..f369b04e3 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,8 @@ "ext-curl": "*", "ext-zip": "*", "league/climate": "^3.2", - "antoligy/dom-string-iterators": "^1.0" + "antoligy/dom-string-iterators": "^1.0", + "miljar/php-exif": "^0.6.3" }, "require-dev": { "codeception/codeception": "^2.1", diff --git a/composer.lock b/composer.lock index bafa8c4b7..ec54681a7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "f9e602a9833bae4ef67c9545e72fcfbc", + "content-hash": "b418b22b918df3b62e5e372de49f13c4", "packages": [ { "name": "antoligy/dom-string-iterators", @@ -697,6 +697,61 @@ ], "time": "2017-01-05T08:46:19+00:00" }, + { + "name": "miljar/php-exif", + "version": "v0.6.3", + "source": { + "type": "git", + "url": "https://github.com/PHPExif/php-exif.git", + "reference": "e43cc30608824d7f3466653b52bbbb71874b5b02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPExif/php-exif/zipball/e43cc30608824d7f3466653b52bbbb71874b5b02", + "reference": "e43cc30608824d7f3466653b52bbbb71874b5b02", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpmd/phpmd": "~2.2", + "phpunit/phpunit": "3.7.*", + "satooshi/php-coveralls": "~0.6", + "sebastian/phpcpd": "1.4.*@stable", + "squizlabs/php_codesniffer": "1.4.*@stable" + }, + "suggest": { + "ext-exif": "Use exif PHP extension as adapter", + "lib-exiftool": "Use perl lib exiftool as adapter" + }, + "type": "library", + "autoload": { + "psr-0": { + "PHPExif": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tom Van Herreweghe", + "homepage": "http://theanalogguy.be", + "role": "Developer" + } + ], + "description": "Object-Oriented EXIF parsing", + "keywords": [ + "IPTC", + "exif", + "exiftool", + "jpeg", + "tiff" + ], + "time": "2017-02-06T14:40:26+00:00" + }, { "name": "monolog/monolog", "version": "1.22.1", @@ -918,16 +973,16 @@ }, { "name": "rockettheme/toolbox", - "version": "1.3.3", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/rockettheme/toolbox.git", - "reference": "8eec637fdd60e546fb98db8b0409acc897baaccf" + "reference": "416b854c0c3743a1a69edfa685b7bf7514bc1f93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/8eec637fdd60e546fb98db8b0409acc897baaccf", - "reference": "8eec637fdd60e546fb98db8b0409acc897baaccf", + "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/416b854c0c3743a1a69edfa685b7bf7514bc1f93", + "reference": "416b854c0c3743a1a69edfa685b7bf7514bc1f93", "shasum": "" }, "require": { @@ -962,7 +1017,7 @@ "php", "rockettheme" ], - "time": "2016-10-06T18:02:45+00:00" + "time": "2017-05-15T17:46:25+00:00" }, { "name": "seld/cli-prompt", @@ -1550,16 +1605,16 @@ }, { "name": "codeception/codeception", - "version": "2.2.10", + "version": "2.2.11", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "c32a3f92834db08ceedb4666ea2265c3aa43396e" + "reference": "a8681b416921ae282ccca2c485d75a3ed6756080" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/c32a3f92834db08ceedb4666ea2265c3aa43396e", - "reference": "c32a3f92834db08ceedb4666ea2265c3aa43396e", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/a8681b416921ae282ccca2c485d75a3ed6756080", + "reference": "a8681b416921ae282ccca2c485d75a3ed6756080", "shasum": "" }, "require": { @@ -1570,9 +1625,10 @@ "guzzlehttp/guzzle": ">=4.1.4 <7.0", "guzzlehttp/psr7": "~1.0", "php": ">=5.4.0 <8.0", - "phpunit/php-code-coverage": ">=2.2.4 <5.0", + "phpunit/php-code-coverage": ">=2.2.4 <6.0", "phpunit/phpunit": ">4.8.20 <6.0", - "sebastian/comparator": "~1.1", + "phpunit/phpunit-mock-objects": ">2.3 <5.0", + "sebastian/comparator": ">1.1 <3.0", "sebastian/diff": "^1.4", "stecman/symfony-console-completion": "^0.7.0", "symfony/browser-kit": ">=2.7 <4.0", @@ -1639,7 +1695,7 @@ "functional testing", "unit testing" ], - "time": "2017-03-25T03:19:52+00:00" + "time": "2017-05-11T21:07:05+00:00" }, { "name": "doctrine/instantiator", diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index 7c6b0fb0f..613677a88 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -930,6 +930,17 @@ form: validate: type: bool + media.auto_metadata_exif: + type: toggle + label: PLUGIN_ADMIN.ENABLE_AUTO_METADATA + help: PLUGIN_ADMIN.ENABLE_AUTO_METADATA_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + media.allowed_fallback_types: diff --git a/system/config/system.yaml b/system/config/system.yaml index c1af568f9..5d6ca443f 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -121,10 +121,11 @@ images: auto_fix_orientation: false # Automatically fix the image orientation based on the Exif data media: - enable_media_timestamp: false # Enable media timetsamps + enable_media_timestamp: false # Enable media timestamps upload_limit: 0 # Set maximum upload size in bytes (0 is unlimited) unsupported_inline_types: [] # Array of supported media types to try to display inline allowed_fallback_types: [] # Array of allowed media types of files found if accessed via Page route + auto_metadata_exif: false # Automatically create metadata files from Exif data where possible session: enabled: true # Enable Session support diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index 1c639e830..304838d17 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -45,6 +45,7 @@ class Grav extends Container 'Grav\Common\Service\PageServiceProvider', 'Grav\Common\Service\OutputServiceProvider', 'browser' => 'Grav\Common\Browser', + 'exif' => 'Grav\Common\Helpers\Exif', 'Grav\Common\Service\StreamsServiceProvider', 'Grav\Common\Service\ConfigServiceProvider', 'inflector' => 'Grav\Common\Inflector', diff --git a/system/src/Grav/Common/Helpers/Exif.php b/system/src/Grav/Common/Helpers/Exif.php new file mode 100644 index 000000000..9e7a423ae --- /dev/null +++ b/system/src/Grav/Common/Helpers/Exif.php @@ -0,0 +1,27 @@ +reader = \PHPExif\Reader\Reader::factory(\PHPExif\Reader\Reader::TYPE_NATIVE); + } else { + if (Grav::instance()['config']->get('system.media.auto_metadata_exif')) { + throw new \Exception('Please enable the Exif extension for PHP or disable Exif support in Grav system configuration'); + } + } + } +} diff --git a/system/src/Grav/Common/Page/Media.php b/system/src/Grav/Common/Page/Media.php index afff55f2f..3c5256a49 100644 --- a/system/src/Grav/Common/Page/Media.php +++ b/system/src/Grav/Common/Page/Media.php @@ -8,9 +8,12 @@ namespace Grav\Common\Page; +use Grav\Common\Grav; use Grav\Common\Page\Medium\AbstractMedia; use Grav\Common\Page\Medium\GlobalMedia; use Grav\Common\Page\Medium\MediumFactory; +use RocketTheme\Toolbox\File\File; +use Symfony\Component\Yaml\Yaml; class Media extends AbstractMedia { @@ -18,6 +21,8 @@ class Media extends AbstractMedia protected $path; + protected $standard_exif = ['FileSize', 'MimeType', 'height', 'width']; + /** * @param $path */ @@ -58,6 +63,8 @@ class Media extends AbstractMedia */ protected function init() { + $config = Grav::instance()['config']; + $exif = Grav::instance()['exif']; // Handle special cases where page doesn't exist in filesystem. if (!is_dir($this->path)) { @@ -71,7 +78,7 @@ class Media extends AbstractMedia /** @var \DirectoryIterator $info */ foreach ($iterator as $path => $info) { // Ignore folders and Markdown files. - if (!$info->isFile() || $info->getExtension() == 'md' || $info->getBasename()[0] === '.') { + if (!$info->isFile() || $info->getExtension() === 'md' || $info->getBasename()[0] === '.') { continue; } @@ -116,6 +123,23 @@ class Media extends AbstractMedia continue; } + // Read/store Exif metadata as required + if (!empty($types['base']) && $medium->get('mime') === 'image/jpeg' && empty($types['meta']) && $config->get('system.media.auto_metadata_exif')) { + $file_path = $types['base']['file']; + $meta = $exif->reader->read($file_path); + + if ($meta) { + $meta_path = $file_path . '.meta.yaml'; + $meta_data = $meta->getData(); + $meta_trimmed = array_diff_key($meta_data, array_flip($this->standard_exif)); + if ($meta_trimmed) { + $file = File::instance($meta_path); + $file->save(Yaml::dump($meta_trimmed)); + $types['meta']['file'] = $meta_path; + } + } + } + if (!empty($types['meta'])) { $medium->addMetaFile($types['meta']['file']); } diff --git a/system/src/Grav/Common/Page/Medium/Medium.php b/system/src/Grav/Common/Page/Medium/Medium.php index b1ca2bc03..77396d695 100644 --- a/system/src/Grav/Common/Page/Medium/Medium.php +++ b/system/src/Grav/Common/Page/Medium/Medium.php @@ -49,6 +49,11 @@ class Medium extends Data implements RenderableInterface */ protected $styleAttributes = []; + /** + * @var array + */ + protected $metadata = []; + /** * Construct. * @@ -77,6 +82,16 @@ class Medium extends Data implements RenderableInterface return new Data($this->items); } + /** + * Returns an array containing just the metadata + * + * @return array + */ + public function metadata() + { + return $this->metadata; + } + /** * Add meta file for the medium. * @@ -84,7 +99,8 @@ class Medium extends Data implements RenderableInterface */ public function addMetaFile($filepath) { - $this->merge((array)CompiledYamlFile::instance($filepath)->content()); + $this->metadata = (array)CompiledYamlFile::instance($filepath)->content(); + $this->merge($this->metadata); } /** diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/TwigExtension.php index 96f26020b..71407c8b1 100644 --- a/system/src/Grav/Common/Twig/TwigExtension.php +++ b/system/src/Grav/Common/Twig/TwigExtension.php @@ -125,6 +125,7 @@ class TwigExtension extends \Twig_Extension new \Twig_SimpleFunction('redirect_me', [$this, 'redirectFunc']), new \Twig_SimpleFunction('range', [$this, 'rangeFunc']), new \Twig_SimpleFunction('isajaxrequest', [$this, 'isAjaxFunc']), + new \Twig_SimpleFunction('exif', [$this, 'exifFunc']), ]; } @@ -137,7 +138,7 @@ class TwigExtension extends \Twig_Extension */ public function fieldNameFilter($str) { - $path = explode('.', $str); + $path = explode('.', rtrim($str, '.')); return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : ''); } @@ -956,4 +957,27 @@ class TwigExtension extends \Twig_Extension !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'); } + + /** + * Get's the Exif data for a file + * + * @param $image + * @param bool $raw + * @return mixed + */ + public function exifFunc($image, $raw = false) + { + if (file_exists($image)) { + + $exif_data = $this->grav['exif']->reader->read($image); + + if ($exif_data) { + if ($raw) { + return $exif_data->getRawData(); + } else { + return $exif_data->getData(); + } + } + } + } } diff --git a/system/src/Grav/Console/Cli/CleanCommand.php b/system/src/Grav/Console/Cli/CleanCommand.php index 4445870c9..feaddedce 100644 --- a/system/src/Grav/Console/Cli/CleanCommand.php +++ b/system/src/Grav/Console/Cli/CleanCommand.php @@ -41,13 +41,7 @@ class CleanCommand extends Command 'user/plugins/email/vendor/swiftmailer/swiftmailer/notes', 'user/plugins/email/vendor/swiftmailer/swiftmailer/doc', 'user/themes/antimatter/.sass-cache', - 'vendor/donatj/phpuseragentparser/.git', - 'vendor/donatj/phpuseragentparser/.gitignore', - 'vendor/donatj/phpuseragentparser/.travis.yml', - 'vendor/donatj/phpuseragentparser/composer.json', - 'vendor/donatj/phpuseragentparser/phpunit.xml.dist', - 'vendor/donatj/phpuseragentparser/Tests', - 'vendor/donatj/phpuseragentparser/Tools', + 'vendor/antoligy/dom-string-iterators/composer.json', 'vendor/doctrine/cache/.travis.yml', 'vendor/doctrine/cache/build.properties', 'vendor/doctrine/cache/build.xml', @@ -57,6 +51,16 @@ class CleanCommand extends Command 'vendor/doctrine/cache/.gitignore', 'vendor/doctrine/cache/.git', 'vendor/doctrine/cache/tests', + 'vendor/doctrine/collections/composer.json', + 'vendor/doctrine/collections/phpunit.xml.dist', + 'vendor/doctrine/collections/tests', + 'vendor/donatj/phpuseragentparser/.git', + 'vendor/donatj/phpuseragentparser/.gitignore', + 'vendor/donatj/phpuseragentparser/.travis.yml', + 'vendor/donatj/phpuseragentparser/composer.json', + 'vendor/donatj/phpuseragentparser/phpunit.xml.dist', + 'vendor/donatj/phpuseragentparser/Tests', + 'vendor/donatj/phpuseragentparser/Tools', 'vendor/erusev/parsedown/composer.json', 'vendor/erusev/parsedown/phpunit.xml.dist', 'vendor/erusev/parsedown/.travis.yml', @@ -132,6 +136,12 @@ class CleanCommand extends Command 'vendor/symfony/console/.git', 'vendor/symfony/console/Tester', 'vendor/symfony/console/Tests', + 'vendor/symfony/debug/.gitignore', + 'vendor/symfony/debug/.git', + 'vendor/symfony/debug/phpunit.xml.dist', + 'vendor/symfony/debug/composer.json', + 'vendor/symfony/debug/Tests', + 'vendor/symfony/debug/Resources', 'vendor/symfony/event-dispatcher/.git', 'vendor/symfony/event-dispatcher/.gitignore', 'vendor/symfony/event-dispatcher/composer.json',