├── tests ├── files │ ├── ExifTool.jpg │ ├── pixelWithIcc.jpg │ ├── plop │ │ └── CanonRaw.cr2 │ ├── empty.xml │ ├── simplefile.xml │ └── multiplefile.xml ├── bootstrap.php └── lib │ └── PHPExiftool │ ├── Test │ ├── Command │ │ └── PreviewExtractor.php │ ├── Driver │ │ ├── Metadata │ │ │ ├── MetadataBagTest.php │ │ │ └── MetadataTest.php │ │ ├── Value │ │ │ ├── MonoTest.php │ │ │ ├── MultiTest.php │ │ │ └── BinaryTest.php │ │ ├── TagGroupFactoryTest.php │ │ └── TagGroupTest.php │ ├── ExiftoolTest.php │ ├── InformationDumperTest.php │ ├── AbstractPreviewExtractorTest.php │ └── FileEntityTest.php │ ├── PHPExiftoolTest.php │ ├── RDFParserTest.php │ ├── ReaderTest.php │ └── WriterTest.php ├── lib └── PHPExiftool │ ├── ClassUtils │ ├── TypeBuilder.php │ ├── ClassProperty.php │ ├── tagGroupBuilder.php │ └── Builder.php │ ├── Driver │ ├── HelperInterface.php │ ├── Value │ │ ├── ValueInterface.php │ │ ├── Mono.php │ │ ├── Multi.php │ │ └── Binary.php │ ├── AbstractType.php │ ├── Metadata │ │ ├── MetadataBag.php │ │ └── Metadata.php │ ├── TagGroupInterface.php │ ├── TagGroupFactory.php │ └── AbstractTagGroup.php │ ├── Exception │ ├── ExceptionInterface.php │ ├── LogicException.php │ ├── RuntimeException.php │ ├── TagUnknown.php │ ├── InvalidArgumentException.php │ ├── ParseErrorException.php │ ├── EmptyCollectionException.php │ └── DirectoryNotFoundException.php │ ├── Factory.php │ ├── FileEntity.php │ ├── PreviewExtractor.php │ ├── Tool │ └── Command │ │ ├── DumpCommand.php │ │ └── ClassesBuilderCommand.php │ ├── PHPExiftool.php │ ├── Exiftool.php │ ├── RDFParser.php │ ├── Writer.php │ ├── Reader.php │ └── InformationDumper.php ├── .gitignore ├── .travis.yml ├── phpunit.xml.dist ├── phpunit-no-cc.xml.dist ├── bin └── console ├── LICENSE ├── vendors.win.php ├── CHANGELOG.md ├── composer.json └── README.md /tests/files/ExifTool.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemy-fr/PHPExiftool/HEAD/tests/files/ExifTool.jpg -------------------------------------------------------------------------------- /tests/files/pixelWithIcc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemy-fr/PHPExiftool/HEAD/tests/files/pixelWithIcc.jpg -------------------------------------------------------------------------------- /tests/files/plop/CanonRaw.cr2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alchemy-fr/PHPExiftool/HEAD/tests/files/plop/CanonRaw.cr2 -------------------------------------------------------------------------------- /lib/PHPExiftool/ClassUtils/TypeBuilder.php: -------------------------------------------------------------------------------- 1 | isClassesGenerated()) { 11 | $x->generateClasses([InformationDumper::LISTOPTION_MWG], ['en']); 12 | } 13 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Exception; 13 | 14 | interface ExceptionInterface 15 | { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /tests/files/empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Exception; 13 | 14 | class LogicException extends \LogicException implements ExceptionInterface 15 | { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Exception; 13 | 14 | class RuntimeException extends \RuntimeException implements ExceptionInterface 15 | { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Exception/TagUnknown.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Exception; 13 | 14 | use Exception; 15 | 16 | class TagUnknown extends Exception implements ExceptionInterface 17 | { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | php: 6 | - 7.4 7 | - 8.2 8 | 9 | matrix: 10 | fast_finish: true 11 | include: 12 | - php: 5.6 13 | env: PREFER_LOWEST="--prefer-lowest" 14 | 15 | pre_install: 16 | - phpenv config-rm xdebug.ini 17 | install: 18 | - composer update --prefer-source --no-interaction $PREFER_LOWEST 19 | - if [ -n "$PREFER_LOWEST" ];then composer update phpunit/phpunit --prefer-source --no-interaction --with-dependencies;fi 20 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Exception; 13 | 14 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 15 | { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Exception/ParseErrorException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Exception; 13 | 14 | use Exception; 15 | 16 | class ParseErrorException extends Exception implements ExceptionInterface 17 | { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Exception/EmptyCollectionException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Exception; 13 | 14 | use Exception; 15 | 16 | class EmptyCollectionException extends Exception implements ExceptionInterface 17 | { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Exception/DirectoryNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Exception; 13 | 14 | use Exception; 15 | 16 | class DirectoryNotFoundException extends Exception implements ExceptionInterface 17 | { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Driver/Value/ValueInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Driver\Value; 13 | 14 | interface ValueInterface 15 | { 16 | const TYPE_BINARY = 'binary'; 17 | const TYPE_MONO = 'mono'; 18 | const TYPE_MULTI = 'multi'; 19 | 20 | public function set($value); 21 | 22 | public function getType(); 23 | 24 | public function asString(); 25 | 26 | public function asArray(); 27 | } 28 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Driver/AbstractType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Driver; 13 | 14 | /** 15 | * Exiftool metadata Type Object 16 | * 17 | * @author Romain Neutron - imprec@gmail.com 18 | * @license http://opensource.org/licenses/MIT MIT 19 | */ 20 | abstract class AbstractType implements TypeInterface 21 | { 22 | // use AttributeReflectionTrait; 23 | 24 | protected string $ExiftoolName; 25 | protected string $PHPMap; 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | tests 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /phpunit-no-cc.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | tests 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lib/PHPExiftool/ClassUtils/ClassProperty.php: -------------------------------------------------------------------------------- 1 | name = $name; 17 | } 18 | 19 | public function setVisibility(string $visibility) 20 | { 21 | if(!in_array($visibility, [self::PRIVATE, self::PROTECTED, self::PUBLIC])) { 22 | throw new \TypeError(sprintf("Bad visibility \"%s\" for attribute \"%s\"", $visibility, $this->name)); 23 | } 24 | $this->visibility = $visibility; 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use PHPExiftool\Tool\Command\ClassesBuilderCommand; 14 | use PHPExiftool\Tool\Command\DumpCommand; 15 | use Symfony\Component\Console\Application; 16 | 17 | require __DIR__ . '/../vendor/autoload.php'; 18 | 19 | if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { 20 | echo "You must install composer developer packages to use the commandline dev tool\n"; 21 | exit(1); 22 | } 23 | 24 | $cli = new Application('PHPExiftool', '2.2'); 25 | $cli->addCommands(array(new ClassesBuilderCommand())); 26 | $cli->addCommands(array(new DumpCommand())); 27 | 28 | $cli->run(); 29 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/Command/PreviewExtractor.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test\Command; 12 | 13 | require_once __DIR__ . '/../AbstractPreviewExtractorTest.php'; 14 | 15 | use Monolog\Logger; 16 | use Monolog\Handler\NullHandler; 17 | use lib\PHPExiftool\AbstractPreviewExtractorTest; 18 | use lib\PHPExiftool\Exiftool; 19 | 20 | class PreviewExtractor extends AbstractPreviewExtractorTest 21 | { 22 | 23 | protected function getExiftool() 24 | { 25 | $logger = new Logger('Tests'); 26 | $logger->pushHandler(new NullHandler()); 27 | 28 | return new Exiftool($logger); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is released under MIT License 2 | 3 | Copyright (c) 2015-2016 Alchemy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 13 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Driver/Value/Mono.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Driver\Value; 13 | 14 | class Mono implements ValueInterface 15 | { 16 | protected string $value; 17 | 18 | public function __construct($value = null) 19 | { 20 | $this->set($value); 21 | } 22 | 23 | public function getType(): string 24 | { 25 | return self::TYPE_MONO; 26 | } 27 | 28 | public function set($value): Mono 29 | { 30 | $this->value = (string) $value; 31 | 32 | return $this; 33 | } 34 | 35 | public function asString(): string 36 | { 37 | return $this->value; 38 | } 39 | 40 | public function asArray(): array 41 | { 42 | return [$this->value]; 43 | } 44 | 45 | public function __toString() 46 | { 47 | return $this->value; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /vendors.win.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | /** 13 | * Get all dependencies needed for Phraseanet (Windows Version) 14 | * 15 | * Set the variables gitDir and phpDir with a trailing slash if it is not set in Windows' %PATH% 16 | * For example : 17 | * $phpDir="c:/php5310/" 18 | */ 19 | call_user_func(function() 20 | { 21 | $phpDir = ""; 22 | 23 | chdir(__DIR__); 24 | 25 | set_time_limit(0); 26 | 27 | $composer = __DIR__ . '/composer.phar'; 28 | 29 | if ( ! file_exists($composer)) 30 | { 31 | file_put_contents($composer, file_get_contents('http://getcomposer.org/installer'), LOCK_EX); 32 | system($phpDir . 'php ' . $composer . ' install'); 33 | } 34 | 35 | system($phpDir . 'php ' . $composer . ' self-update'); 36 | system($phpDir . 'php ' . $composer . ' update'); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/Driver/Metadata/MetadataBagTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test\Driver\Metadata; 12 | 13 | use PHPExiftool\Driver\Metadata\MetadataBag; 14 | use PHPUnit\Framework\TestCase; 15 | 16 | class MetadataBagTest extends TestCase 17 | { 18 | /** 19 | * @var MetadataBag 20 | */ 21 | protected $object; 22 | 23 | protected function setUp(): void 24 | { 25 | $this->object = new MetadataBag(); 26 | } 27 | 28 | /** 29 | * @covers MetadataBag::filterKeysByRegExp 30 | */ 31 | public function testFilterKeysByRegExp() 32 | { 33 | $this->object->set('oneKey', 'oneValue'); 34 | $this->object->set('oneSecondKey', 'anotherValue'); 35 | $this->object->set('thirdKey', 'thirdValue'); 36 | 37 | $this->assertEquals(2, count($this->object->filterKeysByRegExp('/one.*/'))); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/PHPExiftoolTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool; 12 | 13 | use PHPExiftool\Exception\DirectoryNotFoundException; 14 | use PHPExiftool\PHPExiftool; 15 | use PHPExiftool\Reader; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | 19 | class PHPExiftoolTest extends TestCase { 20 | 21 | private ?PHPExiftool $PHPExiftool = null; 22 | protected static string $tmpDir = ""; 23 | protected static bool $disableSymLinkTest = false; 24 | 25 | /** 26 | * @covers PHPExiftool::__construct 27 | */ 28 | public function testRelativeClassesRootDirectory() 29 | { 30 | $this->expectException(DirectoryNotFoundException::class); 31 | new PHPExiftool("./relative_dir"); 32 | } 33 | 34 | /** 35 | * @covers PHPExiftool::__construct 36 | */ 37 | public function testBadClassesRootDirectory() 38 | { 39 | $this->expectException(DirectoryNotFoundException::class); 40 | new PHPExiftool("/non_existing_dir"); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Driver/Metadata/MetadataBag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Driver\Metadata; 13 | 14 | use Doctrine\Common\Collections\ArrayCollection; 15 | use Doctrine\Common\Collections\Collection; 16 | use Doctrine\Common\Collections\Selectable; 17 | 18 | /** 19 | * Container for Metadatas 20 | * 21 | * @template-implements Collection 22 | * @template-implements Selectable 23 | * 24 | * @author Romain Neutron - imprec@gmail.com 25 | * @license http://opensource.org/licenses/MIT MIT 26 | */ 27 | class MetadataBag extends ArrayCollection 28 | { 29 | 30 | /** 31 | * Returns all the elements which key matches the regexp 32 | * 33 | * @param string $regexp 34 | * @return MetadataBag 35 | */ 36 | public function filterKeysByRegExp(string $regexp): MetadataBag 37 | { 38 | $partitions = $this->partition(function($key, $element) use ($regexp) { 39 | return preg_match($regexp, $key); 40 | }); 41 | 42 | return array_shift($partitions); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/Driver/Value/MonoTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test\Driver\Value; 12 | 13 | use PHPExiftool\Driver\Value\Mono; 14 | use PHPExiftool\Driver\Value\ValueInterface; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class MonoTest extends TestCase 18 | { 19 | /** 20 | * @var Mono 21 | */ 22 | protected $object; 23 | 24 | /** 25 | * @covers Mono::__construct 26 | */ 27 | protected function setUp(): void 28 | { 29 | $this->object = new Mono('Hello !'); 30 | } 31 | 32 | /** 33 | * @covers Mono::getType 34 | */ 35 | public function testGetType() 36 | { 37 | $this->assertEquals(ValueInterface::TYPE_MONO, $this->object->getType()); 38 | } 39 | 40 | /** 41 | * @covers Mono::asString 42 | */ 43 | public function testAsString() 44 | { 45 | $this->assertEquals('Hello !', $this->object->asString()); 46 | } 47 | 48 | /** 49 | * @covers Mono::set 50 | */ 51 | public function testSetValue() 52 | { 53 | $this->object->set('World !'); 54 | $this->assertEquals('World !', $this->object->asString()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Factory.php: -------------------------------------------------------------------------------- 1 | phpExiftool = $phpExiftool; 16 | } 17 | 18 | public function createReader(): Reader 19 | { 20 | return Reader::create( 21 | new Exiftool($this->phpExiftool->getLogger()), 22 | new RDFParser($this->phpExiftool->getClassesRootDirectory(), $this->phpExiftool->getLogger()) 23 | ); 24 | } 25 | 26 | public function createWriter(): Writer 27 | { 28 | return Writer::create( 29 | new Exiftool($this->phpExiftool->getLogger()) 30 | ); 31 | } 32 | 33 | public function createTagGroup(string $tagName): TagGroupInterface 34 | { 35 | return TagGroupFactory::getFromRDFTagname( 36 | $this->phpExiftool->getClassesRootDirectory(), 37 | $tagName, 38 | $this->phpExiftool->getLogger() 39 | ); 40 | } 41 | 42 | public function getHelper(): HelperInterface 43 | { 44 | return TagGroupFactory::loadClass( 45 | $this->phpExiftool->getClassesRootDirectory(), 46 | "TagGroup\\Helper", 47 | $this->phpExiftool->getLogger() 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/Driver/Metadata/MetadataTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test\Driver\Metadata; 12 | 13 | use PHPExiftool\Driver\AbstractTagGroup; 14 | use PHPExiftool\Driver\Value\Mono; 15 | use PHPExiftool\Driver\Metadata\Metadata; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class MetadataTest extends TestCase 19 | { 20 | /** 21 | * @var Metadata 22 | */ 23 | protected $object; 24 | protected $tag; 25 | protected $value; 26 | 27 | /** 28 | * @covers Metadata::__construct 29 | */ 30 | protected function setUp(): void 31 | { 32 | $this->tag = new TagTest(); 33 | $this->value = new Mono('valeur'); 34 | $this->object = new Metadata(new TagTest, $this->value, new \SplFileInfo(__FILE__)); 35 | } 36 | 37 | /** 38 | * @covers Metadata::getTagGroup 39 | */ 40 | public function testGetTag() 41 | { 42 | $this->assertEquals($this->object->getTagGroup(), $this->tag); 43 | } 44 | 45 | /** 46 | * @covers Metadata::getValue 47 | */ 48 | public function testGetValue() 49 | { 50 | $this->assertEquals($this->object->getValue(), $this->value); 51 | } 52 | } 53 | 54 | class TagTest extends AbstractTagGroup 55 | { 56 | 57 | } 58 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Driver/Metadata/Metadata.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Driver\Metadata; 13 | 14 | use PHPExiftool\Driver\TagGroupInterface; 15 | use PHPExiftool\Driver\Value\Mono; 16 | use PHPExiftool\Driver\Value\Multi; 17 | use PHPExiftool\Driver\Value\ValueInterface; 18 | 19 | /** 20 | * Metadata Object to map a TagGroup to a value 21 | * 22 | * @author Romain Neutron - imprec@gmail.com 23 | * @license http://opensource.org/licenses/MIT MIT 24 | */ 25 | class Metadata 26 | { 27 | protected TagGroupInterface $tagGroup; 28 | protected ValueInterface $value; 29 | 30 | public function __construct(TagGroupInterface $tagGroup, ValueInterface $value = NULL) 31 | { 32 | $this->tagGroup = $tagGroup; 33 | if(!$value) { 34 | $value = $tagGroup->isMulti() ? new Multi() : new Mono(); 35 | } 36 | $this->value = $value; 37 | 38 | return $this; 39 | } 40 | 41 | public function getTagGroup(): TagGroupInterface 42 | { 43 | return $this->tagGroup; 44 | } 45 | 46 | public function getValue(): ValueInterface 47 | { 48 | return $this->value; 49 | } 50 | 51 | public function setValue($value): self 52 | { 53 | $this->value->set($value); 54 | 55 | return $this; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/ExiftoolTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test; 12 | 13 | use Exception; 14 | use Monolog\Logger; 15 | use Monolog\Handler\NullHandler; 16 | use PHPExiftool\Exception\RuntimeException; 17 | use PHPExiftool\Exiftool; 18 | use lib\PHPExiftool; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | class ExiftoolTest extends TestCase 22 | { 23 | 24 | /** 25 | * @covers PHPExiftool\Exiftool::executeCommand 26 | * @throws Exception 27 | */ 28 | public function testExecuteCommand() 29 | { 30 | $exiftool = new Exiftool($this->getlogger()); 31 | $this->assertRegExp('/\d+\.\d+/', $exiftool->executeCommand(['-ver'])); 32 | } 33 | 34 | /** 35 | * @covers PHPExiftool\Exiftool::executeCommand 36 | * @covers PHPExiftool\Exception\RuntimeException 37 | * @throws Exception 38 | */ 39 | public function testExecuteCommandFailed() 40 | { 41 | $this->expectException(RuntimeException::class); 42 | $exiftool = new Exiftool($this->getlogger()); 43 | $exiftool->executeCommand(['-prout']); 44 | } 45 | 46 | private function getlogger(): Logger 47 | { 48 | $logger = new Logger('Tests'); 49 | $logger->pushHandler(new NullHandler()); 50 | 51 | return $logger; 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Driver/Value/Multi.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Driver\Value; 13 | 14 | class Multi implements ValueInterface 15 | { 16 | protected array $value = []; 17 | 18 | public function __construct($value = null) 19 | { 20 | if ($value) { 21 | $this->addValue($value); 22 | } 23 | } 24 | 25 | public function getType(): string 26 | { 27 | return self::TYPE_MULTI; 28 | } 29 | 30 | public function addValue($value): Multi 31 | { 32 | $this->value = array_merge($this->value, (array) $value); 33 | 34 | return $this; 35 | } 36 | 37 | public function set($value): Multi 38 | { 39 | $this->value = (array) $value; 40 | 41 | return $this; 42 | } 43 | 44 | public function reset(): Multi 45 | { 46 | $this->value = []; 47 | 48 | return $this; 49 | } 50 | 51 | public function serialize($separator = ' ; '): string 52 | { 53 | return implode($separator, $this->value); 54 | } 55 | 56 | public function asString(): string 57 | { 58 | return $this->serialize(); 59 | } 60 | 61 | public function asArray(): array 62 | { 63 | return $this->value; 64 | } 65 | 66 | public function __toString() 67 | { 68 | return $this->serialize(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/Driver/Value/MultiTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test\Driver\Value; 12 | 13 | use PHPExiftool\Driver\Value\Multi; 14 | use PHPExiftool\Driver\Value\ValueInterface; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class MultiTest extends TestCase 18 | { 19 | /** 20 | * @var Multi 21 | */ 22 | protected $object; 23 | 24 | /** 25 | * @covers Multi::__construct 26 | */ 27 | protected function setUp(): void 28 | { 29 | $this->object = new Multi(array('hello', 'world !')); 30 | } 31 | 32 | /** 33 | * @covers Multi::getType 34 | */ 35 | public function testGetType() 36 | { 37 | $this->assertEquals(ValueInterface::TYPE_MULTI, $this->object->getType()); 38 | } 39 | 40 | /** 41 | * @covers Multi::asArray 42 | */ 43 | public function testAsArray() 44 | { 45 | $this->assertEquals(array('hello', 'world !'), $this->object->asArray()); 46 | } 47 | 48 | /** 49 | * @covers Multi::addValue 50 | */ 51 | public function testAddValue() 52 | { 53 | $this->object->addValue('tim'); 54 | $this->assertEquals(array('hello', 'world !', 'tim'), $this->object->asArray()); 55 | } 56 | 57 | /** 58 | * @covers Multi::reset 59 | */ 60 | public function testReset() 61 | { 62 | $this->object->reset(); 63 | $this->assertEquals(array(), $this->object->asArray()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | --------- 3 | * Unreleased 4 | * *No unreleased changes* 5 | 6 | * 0.7 7 | * Add timeout to reader and writer (default 60 sec.) 8 | 9 | * 0.6.0 (2016-09-29) 10 | 11 | * Add support for Symfony 3 components (@temp) 12 | * Remove support for old PHP versions (5.3.x, 5.4.X) 13 | * fix ValueInterface::TYPE_BINARY const typo (@CedCannes) 14 | 15 | * 0.5.1 (2016-02-05) 16 | 17 | * Update to exiftool 10.10 which is a production release (@bburnichon) 18 | * Add support to external ExifTool binary (@gioid) 19 | * Fix README (@bburnichon & @michalsanger) 20 | 21 | * 0.5.0 (2015-11-30) 22 | 23 | * add compatibility up to PHP7 (@bburnichon) 24 | * all classes generated with included exiftool (10.07) (@bburnichon) 25 | * add progress bar to command (@bburnichon) 26 | * Make TagFactory extendable (@bburnichon) 27 | * Added option "--with-mwg" to classes-builder (@jygaulier) 28 | * Added a "copy" method in writer: Copy metadata from one file to another (@jygaulier) 29 | 30 | * 0.4.1 (2014-09-19) 31 | 32 | * Fix some incompatibilities with exiftool v9.70 (@nlegoff) 33 | 34 | * 0.4.0 (2014-09-15) 35 | 36 | * Update to exiftool 9.70 37 | * Fix type mapping (@SimonSimCity) 38 | * Fix common args order (@nlegoff) 39 | 40 | * 0.3.0 (2013-08-07) 41 | 42 | * Add possibility to erase metadata except ICC profile. 43 | * Fix sync mode support. 44 | * Add support for Photoshop preview extraction. 45 | 46 | * 0.2.2 (2013-04-17) 47 | 48 | * Add missing files 49 | 50 | * 0.2.1 (2013-04-16) 51 | 52 | * Add Tags serialization through JMS Serializer 53 | 54 | * 0.2.0 (2013-04-16) 55 | 56 | * Use exiftool 9.15 57 | * Fix documentation examples 58 | 59 | * 0.1.0 (2013-01-30) 60 | 61 | * First stable version. 62 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Driver/TagGroupInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Driver; 13 | 14 | /** 15 | * @author Romain Neutron - imprec@gmail.com 16 | * @license http://opensource.org/licenses/MIT MIT 17 | */ 18 | interface TagGroupInterface 19 | { 20 | 21 | /** 22 | * Return TagGroup Id - TagGroup dependant 23 | * 24 | * @return string 25 | */ 26 | public function getId(): string; 27 | 28 | /** 29 | * Return the tagGroup name 30 | * 31 | * @return string 32 | */ 33 | public function getName(): string; 34 | 35 | /** 36 | * A small string about the TagGroup 37 | * 38 | * @return string 39 | */ 40 | public function getDescription(string $lng = 'en'): ?string; 41 | 42 | /** 43 | * An array of available values for this tag 44 | * Other values should not be allowed 45 | * 46 | * @return array 47 | */ 48 | // public function getValues(): array; 49 | 50 | /** 51 | * Returns true if the TagGroup handles list values 52 | * 53 | * @return boolean 54 | */ 55 | public function isMulti(): bool; 56 | 57 | /** 58 | * Returns true if the value is binary 59 | * 60 | * @return bool 61 | */ 62 | public function isBinary(): bool; 63 | 64 | public function getPhpType(): ?string; 65 | 66 | /** 67 | * Returns true if the value can be written in the tag 68 | * 69 | * @return bool 70 | */ 71 | public function isWritable(): bool; 72 | 73 | public function getMaxLength(): int; 74 | } 75 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Driver/Value/Binary.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Driver\Value; 13 | 14 | use PHPExiftool\Exception\InvalidArgumentException; 15 | 16 | class Binary implements ValueInterface 17 | { 18 | protected string $value; 19 | 20 | public function __construct($value) 21 | { 22 | $this->set($value); 23 | } 24 | 25 | public function getType(): string 26 | { 27 | return self::TYPE_BINARY; 28 | } 29 | 30 | public function asString(): string 31 | { 32 | return $this->value; 33 | } 34 | 35 | public function asArray(): array 36 | { 37 | return [$this->value]; 38 | } 39 | 40 | public function asBase64(): string 41 | { 42 | return base64_encode($this->value); 43 | } 44 | 45 | public function set($value): Binary 46 | { 47 | $this->value = $value; 48 | 49 | return $this; 50 | } 51 | 52 | public function setBase64Value(string $base64Value): Binary 53 | { 54 | if (false === $value = base64_decode($base64Value, true)) { 55 | throw new InvalidArgumentException('The value should be base64 encoded'); 56 | } 57 | 58 | $this->value = $value; 59 | 60 | return $this; 61 | } 62 | 63 | public static function loadFromBase64($base64Value): Binary 64 | { 65 | if (false === $value = base64_decode($base64Value, true)) { 66 | throw new InvalidArgumentException('The value should be base64 encoded'); 67 | } 68 | 69 | return new static($value); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alchemy/phpexiftool", 3 | "type": "library", 4 | "description": "Exiftool driver for PHP", 5 | "keywords": ["metadata","exiftool"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Romain Neutron", 10 | "email": "imprec@gmail.com", 11 | "homepage": "http://www.lickmychip.com/" 12 | }, 13 | { 14 | "name": "Benoit Burnichon", 15 | "email": "bburnichon@alchemy.fr", 16 | "role": "Lead Developer" 17 | } 18 | ], 19 | "repositories": [ 20 | { 21 | "type": "package", 22 | "package": { 23 | "name": "exiftool/exiftool", 24 | "version": "12", 25 | "source": { 26 | "url": "https://github.com/exiftool/exiftool", 27 | "type": "git", 28 | "reference": "12.42" 29 | } 30 | } 31 | } 32 | ], 33 | "require": { 34 | "php": "^7.4 || ^8.2", 35 | "doctrine/cache": "^1.0", 36 | "doctrine/collections": "^1.0", 37 | "exiftool/exiftool": "*", 38 | "symfony/console": "^5 || ^6.2", 39 | "symfony/css-selector": "^5 || ^6.2", 40 | "symfony/dom-crawler": "^5 || ^6.2", 41 | "symfony/process": "^5 || ^6", 42 | "ext-dom": "*", 43 | "cache/array-adapter": "^1.2", 44 | "ext-json": "*", 45 | "symfony/monolog-bridge": "^5.4 || ^6.2" 46 | }, 47 | "suggest": { 48 | "jms/serializer": "To serialize tags", 49 | "symfony/yaml": "To serialize tags in Yaml format" 50 | }, 51 | "require-dev": { 52 | "jms/serializer": "~3", 53 | "phpunit/phpunit": "^9.6.7", 54 | "symfony/finder": "^5", 55 | "symfony/yaml": "^5 || ^6" 56 | }, 57 | "autoload": { 58 | "psr-4": { 59 | "PHPExiftool\\": "lib/PHPExiftool/" 60 | } 61 | }, 62 | "autoload-dev": { 63 | "psr-4": { 64 | } 65 | }, 66 | "scripts": { 67 | "test": "./vendor/bin/phpunit" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/InformationDumperTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test; 12 | 13 | use Monolog\Logger; 14 | use Monolog\Handler\NullHandler; 15 | use PHPExiftool\Exception\InvalidArgumentException; 16 | use PHPExiftool\InformationDumper; 17 | use PHPExiftool\Exiftool; 18 | use PHPExiftool\Exception\DirectoryNotFoundException; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | class InformationDumperTest extends TestCase 22 | { 23 | /** 24 | * @var InformationDumper 25 | */ 26 | protected $object; 27 | 28 | protected function setUp(): void 29 | { 30 | $logger = new Logger('Tests'); 31 | $logger->pushHandler(new NullHandler()); 32 | 33 | $this->object = new InformationDumper(new Exiftool($logger), "/tmp", "PHPExiftool\\Driver"); 34 | } 35 | 36 | /** 37 | * @covers InformationDumper::listDatas 38 | */ 39 | public function testListDatas() 40 | { 41 | $this->object->listDatas(); 42 | } 43 | 44 | /** 45 | * @covers InformationDumper::listDatas 46 | * @covers InvalidArgumentException 47 | */ 48 | public function testListDatasInvalidType() 49 | { 50 | $this->expectException(InvalidArgumentException::class); 51 | $this->object->listDatas('Scrooge'); 52 | } 53 | 54 | /** 55 | * @covers InformationDumper::listDatas 56 | * @covers DirectoryNotFoundException 57 | */ 58 | public function testBadDirectory() 59 | { 60 | $this->markTestIncomplete( 61 | 'DirectoryNotFoundException cannot be forced because directory is created.' 62 | ); 63 | 64 | $this->expectException(DirectoryNotFoundException::class); 65 | 66 | $logger = new Logger('Tests'); 67 | $logger->pushHandler(new NullHandler()); 68 | new InformationDumper(new Exiftool($logger), "./unknownDir/foo", "PHPExiftool\\Driver"); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/AbstractPreviewExtractorTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test; 12 | 13 | use lib\PHPExiftool\PreviewExtractor; 14 | use PHPExiftool\Exception\LogicException; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | abstract class AbstractPreviewExtractorTest extends TestCase 18 | { 19 | 20 | /** 21 | * @covers PHPExiftool\PreviewExtractor::extract 22 | */ 23 | public function testExtract() 24 | { 25 | $extractor = new PreviewExtractor($this->getExiftool()); 26 | 27 | $tmpDir = sys_get_temp_dir() . '/tests' . mt_rand(10000, 99999); 28 | 29 | mkdir($tmpDir); 30 | 31 | $files = $extractor->extract(__DIR__ . '/../../../files/ExifTool.jpg', $tmpDir); 32 | 33 | $this->assertInstanceOf('\\DirectoryIterator', $files); 34 | 35 | $n = 0; 36 | $unlinks = array(); 37 | 38 | foreach ($files as $file) { 39 | if ($file->isDot() || $file->isDir()) { 40 | continue; 41 | } 42 | 43 | $unlinks[] = $file->getPathname(); 44 | $n ++; 45 | } 46 | 47 | foreach ($unlinks as $u) { 48 | unlink($u); 49 | } 50 | 51 | $this->assertEquals(1, $n); 52 | } 53 | 54 | public function testExtractWrongFile() 55 | { 56 | $extractor = new PreviewExtractor($this->getExiftool()); 57 | 58 | $tmpDir = sys_get_temp_dir() . '/tests' . mt_rand(10000, 99999); 59 | 60 | $this->expectException(LogicException::class); 61 | $extractor->extract(__DIR__ . '/ExifTool.jpg', $tmpDir); 62 | } 63 | 64 | public function testExtractWrongDir() 65 | { 66 | $extractor = new PreviewExtractor($this->getExiftool()); 67 | 68 | $tmpDir = sys_get_temp_dir() . '/tests' . mt_rand(10000, 99999); 69 | 70 | $this->expectException(LogicException::class); 71 | $extractor->extract(__DIR__ . '/../../../files/ExifTool.jpg', $tmpDir); 72 | } 73 | 74 | abstract protected function getExiftool(); 75 | } 76 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/Driver/TagGroupFactoryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test\Driver; 12 | 13 | use PHPExiftool\Driver\TagGroupFactory; 14 | use PHPExiftool\Driver\TagGroupInterface; 15 | use PHPExiftool\Exception\TagUnknown; 16 | use PHPExiftool\PHPExiftool; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class TagGroupFactoryTest extends TestCase 20 | { 21 | protected TagGroupFactory $object; 22 | private PHPExiftool $PHPExiftool; 23 | 24 | protected function setUp(): void 25 | { 26 | $this->PHPExiftool = new PHPExiftool("/tmp"); 27 | } 28 | 29 | private function createTagGroup(string $tagName): TagGroupInterface 30 | { 31 | return $this->PHPExiftool->getFactory()->createTagGroup($tagName); 32 | } 33 | 34 | 35 | 36 | /** 37 | * @covers TagGroupFactory::GetFromRDFTagname 38 | * @covers TagGroupFactory::classnameFromTagname 39 | * @throws TagUnknown 40 | */ 41 | public function testGetFromRDFTagname() 42 | { 43 | $tag = TagGroupFactory::getFromRDFTagname("/tmp", 'IPTC:SupplementalCategories'); 44 | $this->assertInstanceOf(get_class($this->createTagGroup("IPTC:SupplementalCategories")), $tag); 45 | 46 | $tag = TagGroupFactory::getFromRDFTagname("/tmp", 'XMP_exif:ApertureValue'); 47 | $this->assertInstanceOf(get_class($this->createTagGroup("XMP_exif:ApertureValue")), $tag); 48 | 49 | try { 50 | TagGroupFactory::getFromRDFTagname("/tmp", 'XMP_exif:NonExistingTag'); 51 | $this->fail('Should raise a TagUnknown exception'); 52 | } 53 | catch (TagUnknown $e) { 54 | 55 | } 56 | } 57 | 58 | /** 59 | * @covers TagGroupFactory::GetFromRDFTagname 60 | * @covers TagUnknown 61 | * 62 | * @throws TagUnknown 63 | */ 64 | public function testGetFromRDFTagnameFail() 65 | { 66 | $this->expectException(TagUnknown::class); 67 | TagGroupFactory::getFromRDFTagname("/tmp", 'XMP_exif:NonExistingTag'); 68 | } 69 | 70 | /** 71 | * @covers TagGroupFactory::HasFromRDFTagname 72 | */ 73 | public function testHasFromRDFTagname() 74 | { 75 | $this->assertTrue(TagGroupFactory::hasFromRDFTagname("/tmp", 'IPTC:SupplementalCategories')); 76 | $this->assertFalse(TagGroupFactory::hasFromRDFTagname("/tmp", 'XMP_exif:NonExistingTag')); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/FileEntityTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test; 12 | 13 | use Monolog\Handler\NullHandler; 14 | use Monolog\Logger; 15 | use PHPExiftool\FileEntity; 16 | use PHPExiftool\RDFParser; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class FileEntityTest extends TestCase 20 | { 21 | protected FileEntity $object; 22 | protected Logger $logger; 23 | 24 | /** 25 | * @covers FileEntity::__construct 26 | */ 27 | protected function setUp(): void 28 | { 29 | $dom = new \DOMDocument(); 30 | $dom->loadXML(file_get_contents(__DIR__ . '/../../../files/ExifTool.xml')); 31 | 32 | $this->logger = new Logger('Tests'); 33 | $this->logger->pushHandler(new NullHandler()); 34 | 35 | $this->object = new FileEntity('testFile', $dom, new RDFParser("/tmp", $this->logger)); 36 | } 37 | 38 | /** 39 | * @covers FileEntity::getIterator 40 | */ 41 | public function testGetIterator() 42 | { 43 | $this->assertInstanceOf('\\Iterator', $this->object->getIterator()); 44 | } 45 | 46 | /** 47 | * @covers FileEntity::getFile 48 | */ 49 | public function testGetFile() 50 | { 51 | $this->assertIsString($this->object->getFile()); 52 | } 53 | 54 | /** 55 | * @covers FileEntity::getMetadatas 56 | */ 57 | public function testGetMetadatas() 58 | { 59 | $this->assertInstanceOf('\\PHPExiftool\Driver\Metadata\MetadataBag', $this->object->getMetadatas()); 60 | $this->assertCount(348, $this->object->getMetadatas()); 61 | } 62 | 63 | /** 64 | * @covers FileEntity::executeQuery 65 | */ 66 | public function testExecuteQuery() 67 | { 68 | $this->assertInstanceOf('\\PHPExiftool\\Driver\\Value\\Mono', $this->object->executeQuery('IFD0:Copyright')); 69 | $this->assertEquals('Copyright 2004 Phil Harvey', $this->object->executeQuery('IFD0:Copyright')->asString()); 70 | 71 | $this->assertInstanceOf('\\PHPExiftool\\Driver\\Value\\Binary', $this->object->executeQuery('CIFF:FreeBytes')); 72 | 73 | $this->assertInstanceOf('\\PHPExiftool\\Driver\\Value\\Multi', $this->object->executeQuery('XMP-dc:Subject')); 74 | $this->assertEquals(array('ExifTool', 'Test', 'XMP'), $this->object->executeQuery('XMP-dc:Subject')->asArray()); 75 | } 76 | 77 | public function testCacheKey() 78 | { 79 | $o = new FileEntity('bad_{}()/\\@:_chars', new \DOMDocument(), new RDFParser("/tmp", $this->logger)); 80 | $k = $o->getCacheKey(); 81 | $this->assertEquals('bad_%7B%7D%28%29%2F%5C%40%3A_chars', $k); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/PHPExiftool/FileEntity.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool; 13 | 14 | 15 | use Cache\Adapter\PHPArray\ArrayCachePool; 16 | use DOMDocument; 17 | use Exception; 18 | use IteratorAggregate; 19 | use PHPExiftool\Driver\Metadata\MetadataBag; 20 | use PHPExiftool\Driver\Value\ValueInterface; 21 | use Psr\Cache\CacheItemPoolInterface; 22 | use Psr\Cache\InvalidArgumentException; 23 | 24 | 25 | /** 26 | * 27 | * 28 | * @author Romain Neutron - imprec@gmail.com 29 | * @license http://opensource.org/licenses/MIT MIT 30 | */ 31 | class FileEntity implements IteratorAggregate 32 | { 33 | // private DOMDocument $dom; 34 | 35 | private string $file; 36 | 37 | private CacheItemPoolInterface $cache; 38 | 39 | private RDFParser $parser; 40 | 41 | /** 42 | * Construct a new FileEntity 43 | * 44 | * @param string $file 45 | * @param DOMDocument $dom 46 | * @param RDFParser $parser 47 | * @return FileEntity 48 | */ 49 | public function __construct(string $file, DOMDocument $dom, RDFParser $parser) 50 | { 51 | // $this->dom = $dom; 52 | $this->file = $file; 53 | 54 | $this->cache = new ArrayCachePool(); 55 | 56 | $this->parser = $parser->open($dom->saveXML()); 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * @throws InvalidArgumentException 63 | * @throws Exception 64 | */ 65 | public function getIterator() 66 | { 67 | return $this->getMetadatas()->getIterator(); 68 | } 69 | 70 | public function getFile(): string 71 | { 72 | return $this->file; 73 | } 74 | 75 | /** 76 | * 77 | * @return MetadataBag 78 | * @throws InvalidArgumentException 79 | */ 80 | public function getMetadatas(): MetadataBag 81 | { 82 | $key = $this->getCacheKey(); 83 | $ci = $this->cache->getItem($key); 84 | if($ci->isHit()) { 85 | return $ci->get(); 86 | } 87 | 88 | $metadatas = $this->parser->ParseMetadatas(); 89 | $ci->set($metadatas); 90 | 91 | return $metadatas; 92 | } 93 | 94 | public function getCacheKey(): string 95 | { 96 | return urlencode($this->file); // will encode psr6 reserved chars 97 | } 98 | 99 | /** 100 | * Execute a user defined query to retrieve metadata 101 | * 102 | * @param string $query 103 | * 104 | * @return ValueInterface 105 | */ 106 | public function executeQuery(string $query): ValueInterface 107 | { 108 | return $this->parser->Query($query); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/Driver/Value/BinaryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test\Driver\Value; 12 | 13 | use PHPExiftool\Driver\Value\Binary; 14 | use PHPExiftool\Driver\Value\ValueInterface; 15 | use PHPExiftool\Exception\InvalidArgumentException; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class BinaryTest extends TestCase 19 | { 20 | /** 21 | * @var Binary 22 | */ 23 | protected $object; 24 | 25 | /** 26 | * @covers Binary::__construct 27 | */ 28 | protected function setUp(): void 29 | { 30 | $this->object = new Binary('Binary'); 31 | } 32 | 33 | /** 34 | * @covers Binary::getType 35 | */ 36 | public function testGetType() 37 | { 38 | $this->assertEquals(ValueInterface::TYPE_BINARY, $this->object->getType()); 39 | } 40 | 41 | /** 42 | * @covers Binary::asString 43 | */ 44 | public function testAsString() 45 | { 46 | $this->assertEquals('Binary', $this->object->asString()); 47 | } 48 | 49 | /** 50 | * @covers Binary::asBase64 51 | */ 52 | public function testAsBase64() 53 | { 54 | $this->assertEquals(base64_encode('Binary'), $this->object->asBase64()); 55 | } 56 | 57 | /** 58 | * @covers Binary::set 59 | */ 60 | public function testSetValue() 61 | { 62 | $this->object->set('Daisy'); 63 | $this->assertEquals('Daisy', $this->object->asString()); 64 | } 65 | 66 | /** 67 | * @covers Binary::setBase64Value 68 | */ 69 | public function testSetBase64Value() 70 | { 71 | $this->object->setBase64Value('UmlyaSBGaWZpIGV0IExvdWxvdQ=='); 72 | $this->assertEquals('Riri Fifi et Loulou', $this->object->asString()); 73 | } 74 | 75 | /** 76 | * @covers Binary::setBase64Value 77 | * @covers InvalidArgumentException 78 | */ 79 | public function testSetWrongBase64Value() 80 | { 81 | $this->expectException(InvalidArgumentException::class); 82 | $this->object->setBase64Value('Riri Fifi et Loulou !'); 83 | } 84 | 85 | /** 86 | * @covers Binary::loadFromBase64 87 | */ 88 | public function testLoadFromBase64() 89 | { 90 | $object = Binary::loadFromBase64('VW5jbGUgU2Nyb29nZQ=='); 91 | $this->assertEquals('Uncle Scrooge', $object->asString()); 92 | $this->assertEquals('VW5jbGUgU2Nyb29nZQ==', $object->asBase64()); 93 | } 94 | 95 | /** 96 | * @covers Binary::loadFromBase64 97 | * @covers InvalidArgumentException 98 | */ 99 | public function testLoadFromWrongBase64() 100 | { 101 | $this->expectException(InvalidArgumentException::class); 102 | Binary::loadFromBase64('Uncle Scrooge !!!'); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/files/simplefile.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 19 | 8.88 20 | phpunit.xml.dist 21 | . 22 | 1051 bytes 23 | 2012:04:18 23:25:03+02:00 24 | rw-r--r-- 25 | XML 26 | application/xml 27 | false 28 | false 29 | true 30 | true 31 | true 32 | true 33 | false 34 | false 35 | true 36 | false 37 | bootstrap.php 38 | coverage-html 39 | tests/phpunit_report/report 40 | UTF-8 41 | true 42 | false 43 | 35 44 | 70 45 | display_errors 46 | on 47 | PHPExiftool Tests Suite 48 | tests 49 | vendor 50 | tests 51 | lib/PHPExiftool/Driver/Tag/* 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/PHPExiftool/PreviewExtractor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool; 13 | 14 | use DirectoryIterator; 15 | use Exception; 16 | use PHPExiftool\Exception\LogicException; 17 | use PHPExiftool\Exception\RuntimeException; 18 | 19 | class PreviewExtractor // extends Exiftool 20 | { 21 | private $exiftool; 22 | 23 | public function __construct(Exiftool $exiftool) 24 | { 25 | // parent::__construct($exiftool->logger, $exiftool->binaryPath); 26 | $this->exiftool = $exiftool; 27 | } 28 | 29 | public function extract($pathfile, $outputDir): DirectoryIterator 30 | { 31 | if ( ! file_exists($pathfile)) { 32 | throw new LogicException(sprintf('%s does not exists', $pathfile)); 33 | } 34 | 35 | if ( ! is_dir($outputDir) || ! is_writable($outputDir)) { 36 | throw new LogicException(sprintf('%s is not writable', $outputDir)); 37 | } 38 | 39 | // $command = "-if " . escapeshellarg('$photoshopthumbnail') . " -b -PhotoshopThumbnail " 40 | // . "-w " . escapeshellarg(realpath($outputDir) . '/PhotoshopThumbnail%c.jpg') . " -execute " 41 | // . "-if " . escapeshellarg('$jpgfromraw') . " -b -jpgfromraw " 42 | // . "-w " . escapeshellarg(realpath($outputDir) . '/JpgFromRaw%c.jpg') . " -execute " 43 | // . "-if " . escapeshellarg('$previewimage') . " -b -previewimage " 44 | // . "-w " . escapeshellarg(realpath($outputDir) . '/PreviewImage%c.jpg') . " -execute " 45 | // . "-if " . escapeshellarg('$xmp:pageimage') . " -b -xmp:pageimage " 46 | // . "-w " . escapeshellarg(realpath($outputDir) . '/XmpPageimage%c.jpg') . " " 47 | // . "-common_args -q -m " . $pathfile; 48 | 49 | $command = [ 50 | '-if', 51 | '$photoshopthumbnail', 52 | '-b', 53 | '-PhotoshopThumbnail', 54 | '-w', 55 | realpath($outputDir) . '/PhotoshopThumbnail%c.jpg', 56 | '-execute', 57 | '-if', 58 | '$jpgfromraw', 59 | '-b', 60 | '-jpgfromraw', 61 | '-w', 62 | realpath($outputDir) . '/JpgFromRaw%c.jpg', 63 | '-execute', 64 | '-if', 65 | '$previewimage', 66 | '-b', 67 | '-previewimage', 68 | '-w', 69 | realpath($outputDir) . '/PreviewImage%c.jpg', 70 | '-execute', 71 | '-if', 72 | '$xmp:pageimage', 73 | '-b', 74 | '-xmp:pageimage', 75 | '-w', 76 | realpath($outputDir) . '/XmpPageimage%c.jpg', 77 | '-common_args', 78 | '-q', 79 | '-m', 80 | $pathfile 81 | ]; 82 | 83 | try { 84 | $this->exiftool->executeCommand($command); 85 | } 86 | catch (RuntimeException | Exception $e) { 87 | // no-op 88 | } 89 | 90 | return new DirectoryIterator($outputDir); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Tool/Command/DumpCommand.php: -------------------------------------------------------------------------------- 1 | setName('dump') 25 | ->setDescription('Dump metadata from a file') 26 | ->addOption('filter', null, InputOption::VALUE_REQUIRED, "Dump only infos for Id's matching this regexp, e.g. \"^(XMP|FILE)\"") 27 | ->addArgument('file', InputArgument::OPTIONAL, 'The file') 28 | ; 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * @throws Exception 35 | */ 36 | protected function execute(InputInterface $input, OutputInterface $output): int 37 | { 38 | $this->input = $input; 39 | $this->output = $output; 40 | 41 | if(is_null($filter = $input->getOption('filter'))) { 42 | $filter = ''; 43 | } 44 | $filter = '/' . $filter . '/'; 45 | /** 46 | * dump the meta from a file 47 | */ 48 | if($input->getArgument('file')) { 49 | 50 | $logger = new \Symfony\Bridge\Monolog\Logger("PHPExiftool"); 51 | $reader = Reader::create($logger); 52 | $reader->files($input->getArgument('file')); 53 | $metadataBag = $reader->files(__FILE__)->first(); 54 | 55 | /** 56 | * @var Metadata $meta 57 | */ 58 | foreach ($metadataBag as $meta) { 59 | $tagGroup = $meta->getTagGroup(); 60 | $id = $tagGroup->getId(); 61 | if(preg_match($filter, $id)) { 62 | $output->writeln(sprintf("%s (name=\"%s\", phpType=\"%s\") ; %s", 63 | $id, 64 | $tagGroup->getName(), 65 | $tagGroup->getPhpType(), 66 | $tagGroup->getDescription('en') 67 | )); 68 | $output->write($tagGroup->isMulti() ? " multi" : " mono"); 69 | $output->write($tagGroup->isBinary() ? " binary" : ""); 70 | $output->write($tagGroup->isWritable() ? " writable" : " read-only"); 71 | $output->writeln($tagGroup->getMaxLength() !== 0 ? (" maxl=" . $tagGroup->getMaxLength()) : ""); 72 | 73 | $v = $meta->getValue(); 74 | $output->writeln(sprintf(" value: \"%s\"", $v->asString())); 75 | } 76 | } 77 | } 78 | else { 79 | // no file arg ? dump the dictionnary 80 | foreach(PHPExiftool::getKnownTagGroups() as $tagGroup) { 81 | if(preg_match($filter, $tagGroup)) { 82 | $output->writeln($tagGroup); 83 | } 84 | } 85 | } 86 | return 0; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP-Exiftool 2 | 3 | [![Build Status](https://secure.travis-ci.org/alchemy-fr/PHPExiftool.png?branch=master)](http://travis-ci.org/alchemy-fr/PHPExiftool) 4 | 5 | This project is a fork of [phpexiftool/phpexiftool](https://github.com/phpexiftool/phpexiftool). 6 | 7 | PHP Exiftool is an Object Oriented driver for Phil Harvey's Exiftool (see 8 | http://www.sno.phy.queensu.ca/~phil/exiftool/). 9 | Exiftool is a powerful library and command line utility for reading, writing 10 | and editing meta information written in Perl. 11 | 12 | PHPExiftool provides an intuitive object oriented interface to read and write 13 | metadata. 14 | 15 | You will find some example below. 16 | This driver is not suitable for production, it is still under heavy development. 17 | 18 | ## Installation 19 | 20 | The recommended way to install PHP-Exiftool is [through composer](http://getcomposer.org). 21 | 22 | ```JSON 23 | { 24 | "require": { 25 | "alchemy/phpexiftool": "^4.0" 26 | } 27 | } 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Exiftool Reader 33 | 34 | A simple example : how to read metadata from a file: 35 | 36 | ```php 37 | files(__FILE__)->first(); 49 | 50 | foreach ($metadataBag as $metadata) { 51 | if (ValueInterface::TYPE_BINARY === $metadata->getValue()->getType()) { 52 | echo sprintf("\t--> Field %s has binary data" . PHP_EOL, $metadata->getTagGroup()); 53 | } else { 54 | echo sprintf("\t--> Field %s has value(s) %s" . PHP_EOL, $metadata->getTagGroup(), $metadata->getValue()->asString()); 55 | } 56 | } 57 | ``` 58 | 59 | An example with directory inspection : 60 | 61 | ```php 62 | use Monolog\Logger; 63 | use PHPExiftool\Reader; 64 | use PHPExiftool\Driver\Value\ValueInterface; 65 | 66 | $logger = new Logger('exiftool'); 67 | $reader = Reader::create($logger); 68 | 69 | $reader 70 | ->in(array('documents', '/Picture')) 71 | ->extensions(array('doc', 'jpg', 'cr2', 'dng')) 72 | ->exclude(array('test', 'tmp')) 73 | ->followSymLinks(); 74 | 75 | foreach ($reader as $data) { 76 | echo "found file " . $data->getFile() . PHP_EOL; 77 | 78 | foreach ($data as $metadata) { 79 | if (ValueInterface::TYPE_BINARY === $metadata->getValue()->getType()) { 80 | echo sprintf("\t--> Field %s has binary data" . PHP_EOL, $metadata->getTagGroup()); 81 | } else { 82 | echo sprintf("\t--> Field %s has value(s) %s" . PHP_EOL, $metadata->getTagGroup(), $metadata->getValue()->asString()); 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | ### Exiftool Writer 89 | 90 | ```php 91 | add(new Metadata(new ObjectName(), new Mono('Pretty cool subject'))); 107 | 108 | $writer->write('image.jpg', $bag); 109 | ``` 110 | 111 | ## License 112 | 113 | Project licensed under the MIT License 114 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/Test/Driver/TagGroupTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool\Test\Driver; 12 | 13 | use PHPExiftool\Driver\HelperInterface; 14 | use PHPExiftool\Driver\TagGroupFactory; 15 | use PHPExiftool\PHPExiftool; 16 | use PHPUnit\Framework\TestCase; 17 | use Symfony\Component\Finder\Finder; 18 | use PHPExiftool\Driver\TagGroupInterface; 19 | 20 | 21 | class TagGroupTest extends TestCase { 22 | 23 | /** 24 | * @var TagGroupInterface 25 | */ 26 | protected TagGroupInterface $object; 27 | 28 | /** 29 | * @covers AbstractTag::getDescription 30 | * @covers AbstractTag::getGroupName 31 | * @covers AbstractTag::getName 32 | * @covers AbstractTag::getTagname 33 | * @covers AbstractTag::getId 34 | * @covers AbstractTag::getValues 35 | * @covers AbstractTag::isMulti 36 | * @covers AbstractTag::isWritable 37 | * @covers AbstractTag::isBinary 38 | */ 39 | public function testConsistency() 40 | { 41 | $PHPExiftool = new PHPExiftool("/tmp"); 42 | 43 | /** @var HelperInterface $helper */ 44 | $helper = $PHPExiftool->getFactory()->getHelper(); 45 | 46 | //return; 47 | $finder = new Finder(); 48 | $finder->files()->in(array('/tmp/TagGroup/')); 49 | 50 | $n = 0; 51 | foreach ($finder as $file) { 52 | if($file->getFilename() === "Helper.php") { 53 | continue; 54 | } 55 | $n++; 56 | 57 | $tagName = 58 | str_replace( 59 | '/', ':', $file->getRelativePath() . '/' . $file->getFilenameWithoutExtension() 60 | ); 61 | 62 | $tag = TagGroupFactory::getFromRDFTagname("/tmp", $tagName); 63 | 64 | /* @var TagGroupInterface $tag */ 65 | 66 | $this->assertTrue(is_scalar($tag->getId())); 67 | $this->assertTrue(is_scalar($tag->getName())); 68 | $this->assertTrue(is_scalar($tag->getDescription('en'))); 69 | $this->assertTrue(is_bool($tag->isWritable())); 70 | $this->assertTrue(is_bool($tag->isBinary())); 71 | $this->assertTrue(is_bool($tag->isMulti())); 72 | $this->assertTrue(is_int($tag->getMaxLength())); 73 | // $this->assertTrue(is_scalar($tag->getGroupName())); 74 | // $this->assertTrue(is_scalar($tag->getTagname())); 75 | 76 | // if ($tag->getValues() !== null) 77 | // $this->assertTrue(is_array($tag->getValues())); 78 | // 79 | // if ($tag->isMulti()) 80 | // $this->assertTrue($tag->isMulti()); 81 | // else 82 | // $this->assertFalse($tag->isMulti()); 83 | // 84 | // if ($tag->isWritable()) 85 | // $this->assertTrue($tag->isWritable()); 86 | // else 87 | // $this->assertFalse($tag->isWritable(), $tag->getTagname() . " is writable"); 88 | // 89 | // if ($tag->isBinary()) 90 | // $this->assertTrue($tag->isBinary()); 91 | // else 92 | // $this->assertFalse($tag->isBinary()); 93 | 94 | unset($tag); 95 | } 96 | 97 | self::assertEquals(count($helper->getIndex()), $n); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Driver/TagGroupFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Driver; 13 | 14 | use PHPExiftool\Exception\TagUnknown; 15 | use PHPExiftool\InformationDumper; 16 | use PHPExiftool\PHPExiftool; 17 | use Psr\Log\LoggerInterface; 18 | 19 | /** 20 | * Metadata Object for mapping a TagGroup to a value 21 | * 22 | * @author Romain Neutron - imprec@gmail.com 23 | * @license http://opensource.org/licenses/MIT MIT 24 | */ 25 | class TagGroupFactory 26 | { 27 | 28 | /** 29 | * load a class 30 | * 31 | * @param string $tagname 32 | * @param LoggerInterface|null $logger 33 | * @throws TagUnknown 34 | */ 35 | public static function loadClass(string $classesRootDirectory, string $classname, ?LoggerInterface $logger = null) 36 | { 37 | $fullClassname = PHPExiftool::ROOT_NAMESPACE . '\\' . $classname; 38 | 39 | // class loader 40 | if ( !class_exists($fullClassname)) { 41 | $fpath = $classesRootDirectory . '/' . str_replace('\\', '/', $classname) . '.php'; 42 | 43 | if ( !file_exists($fpath)) { 44 | throw new TagUnknown(sprintf("file \"%s\" not found for class \"%s\"", $fpath, $fullClassname)); 45 | } 46 | 47 | include_once $fpath; 48 | 49 | if ( !class_exists($fullClassname)) { 50 | throw new TagUnknown(sprintf("class \"%s\" not found into \"%s\"", $fullClassname, $fpath)); 51 | } 52 | } 53 | 54 | return new $fullClassname; 55 | } 56 | 57 | /** 58 | * Build a TagGroup object based on his id 59 | * 60 | * @param string $tagname 61 | * @param LoggerInterface|null $logger 62 | * @return TagGroupInterface 63 | * @throws TagUnknown 64 | */ 65 | public static function getFromRDFTagname(string $classesRootDirectory, string $tagname, ?LoggerInterface $logger = null): TagGroupInterface 66 | { 67 | $classname = static::classnameFromRDFTagname($tagname, $logger); 68 | 69 | if($logger) { 70 | $logger->debug(sprintf("classnameFromRDFTagname(\"%s\") ==> \"%s\"", $tagname, $classname)); 71 | } 72 | 73 | return self::loadClass($classesRootDirectory, $classname, $logger); 74 | } 75 | 76 | public static function hasFromRDFTagname(string $classesRootDirectory, string $tagname, ?LoggerInterface $logger = null): bool 77 | { 78 | $classname = PHPExiftool::ROOT_NAMESPACE . '\\' . static::classnameFromRDFTagname($tagname, $logger); 79 | 80 | // class loader 81 | if ( !class_exists($classname)) { 82 | $path = str_replace('\\', '/', InformationDumper::tagGroupIdToFQClassname($tagname)); 83 | $fpath = $classesRootDirectory . '/' .PHPExiftool::SUBDIR . '/' . $path . '.php'; 84 | 85 | if ( !file_exists($fpath)) { 86 | return false; 87 | } 88 | 89 | include_once $fpath; 90 | 91 | if ( !class_exists($classname)) { 92 | return false; 93 | } 94 | } 95 | 96 | return true; 97 | } 98 | 99 | protected static function classnameFromRDFTagname(string $RdfName, ?LoggerInterface $logger = null): string 100 | { 101 | $id = str_replace('/rdf:RDF/rdf:Description/', '', $RdfName); 102 | $FQClassname = InformationDumper::tagGroupIdToFQClassname($id); 103 | 104 | if($logger) { 105 | $logger->debug(sprintf("tag id(\"%s\") ==> \"%s\" ; tagGroupIdToFQClassname(\"%s\") ==> \"%s\" ", $RdfName, $id, $id, $FQClassname)); 106 | } 107 | 108 | return PHPExiftool::SUBDIR . '\\' . InformationDumper::tagGroupIdToFQClassname($id); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Driver/AbstractTagGroup.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Driver; 13 | 14 | use Exception; 15 | use JMS\Serializer\Annotation\VirtualProperty; 16 | use JMS\Serializer\Annotation\ExclusionPolicy; 17 | use ReflectionClass; 18 | 19 | 20 | /** 21 | * Abstract TagGroup object 22 | * 23 | * @ExclusionPolicy("all") 24 | * 25 | * @author Romain Neutron - imprec@gmail.com 26 | * @license http://opensource.org/licenses/MIT MIT 27 | */ 28 | abstract class AbstractTagGroup implements TagGroupInterface 29 | { 30 | public const FLAG_AVOID = 1; 31 | public const FLAG_BINARY = 2; 32 | public const FLAG_PERMANENT = 4; 33 | public const FLAG_PROTECTED = 8; 34 | public const FLAG_UNSAFE = 16; 35 | public const FLAG_UNKNOWN = 32; 36 | public const FLAG_LIST = 64; 37 | public const FLAG_MANDATORY = 128; 38 | public const FLAG_BAG = 256; 39 | public const FLAG_SEQ = 512; 40 | public const FLAG_ALT = 1024; 41 | public const FLAG_WRITABLE = 2048; 42 | 43 | // public const GROUP_ID = ''; 44 | protected string $id = ''; 45 | protected int $flags = 0; 46 | protected string $name = ''; 47 | protected bool $isWritable = false; 48 | protected ?string $phpType = null; 49 | protected int $count = 0; 50 | protected array $description = []; 51 | protected array $tags = []; 52 | 53 | public function getId(): string 54 | { 55 | return $this->id; 56 | } 57 | 58 | public function getWriteKey(): string 59 | { 60 | return $this->id; 61 | } 62 | 63 | /** 64 | * Return the tag name 65 | * 66 | * @VirtualProperty 67 | * 68 | * @return string 69 | */ 70 | public function getName(): string 71 | { 72 | return $this->name; 73 | } 74 | 75 | /** 76 | * A small string about the TagGroup 77 | * 78 | * @VirtualProperty 79 | * 80 | * @param string $lng 81 | * @return string|null 82 | */ 83 | public function getDescription(string $lng = 'en'): ?string 84 | { 85 | return array_key_exists($lng, $this->description) ? $this->description[$lng] : null; 86 | } 87 | 88 | /** 89 | * An array of available values for this tag 90 | * Other values should not be allowed 91 | * 92 | * @VirtualProperty 93 | * 94 | * @return array 95 | */ 96 | // public function getValues(): array 97 | // { 98 | // return $this->Values; 99 | // } 100 | 101 | /** 102 | * Returns true if the TagGroup handles list values 103 | * 104 | * @VirtualProperty 105 | * 106 | * @return boolean 107 | */ 108 | public function isMulti(): bool 109 | { 110 | return ($this->flags & self::FLAG_LIST) !== 0; 111 | } 112 | 113 | /** 114 | * Returns true if the value is binary 115 | * 116 | * @VirtualProperty 117 | * 118 | * @return boolean 119 | */ 120 | public function isBinary(): bool 121 | { 122 | return ($this->flags & self::FLAG_BINARY) !== 0; 123 | } 124 | 125 | /** 126 | * Returns true if the value can be written in the tagGroup 127 | * 128 | * @VirtualProperty 129 | * 130 | * @return boolean 131 | */ 132 | public function isWritable(): bool 133 | { 134 | return $this->isWritable; 135 | } 136 | 137 | public function getMaxLength(): int 138 | { 139 | return $this->count; 140 | } 141 | 142 | public function getPhpType(): ?string 143 | { 144 | return $this->phpType; 145 | } 146 | 147 | 148 | /** 149 | * Return the tagname 150 | * 151 | * @return string 152 | */ 153 | public function __toString() 154 | { 155 | return $this->getId(); 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Tool/Command/ClassesBuilderCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\Tool\Command; 13 | 14 | use Exception; 15 | use Monolog\Handler\NullHandler; 16 | use Monolog\Handler\StreamHandler; 17 | use Monolog\Logger; 18 | use PHPExiftool\ClassUtils\tagGroupBuilder; 19 | use PHPExiftool\Exiftool; 20 | use PHPExiftool\InformationDumper; 21 | use PHPExiftool\PHPExiftool; 22 | use Symfony\Component\Console\Command\Command; 23 | use Symfony\Component\Console\Input\InputInterface; 24 | use Symfony\Component\Console\Input\InputOption; 25 | use Symfony\Component\Console\Output\OutputInterface; 26 | 27 | 28 | /** 29 | * 30 | * @author Romain Neutron - imprec@gmail.com 31 | * @license http://opensource.org/licenses/MIT MIT 32 | */ 33 | class ClassesBuilderCommand extends Command 34 | { 35 | protected InputInterface $input; 36 | protected OutputInterface $output; 37 | 38 | /** 39 | * 40 | * @var array 41 | */ 42 | protected array $classes = []; 43 | 44 | protected function configure() 45 | { 46 | $this 47 | ->setName('classes-builder') 48 | ->setDescription('Build TagGroup classes from exiftool documentation.') 49 | ->addOption('with-mwg', '', null, 'Include MWG tags') 50 | ->addOption("path", null, InputOption::VALUE_OPTIONAL, 'Destination root where classes will be generated', "./Driver") 51 | ->addOption("lng", null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Wanted lng(s) for tag(group) descriptions', []) 52 | ->setHelp("Classes will be generated in subdirectory \"".PHPExiftool::SUBDIR."\" relative to path option, eg. ./Driver/".PHPExiftool::SUBDIR." for default path.") 53 | ; 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * @throws Exception 60 | */ 61 | protected function execute(InputInterface $input, OutputInterface $output): int 62 | { 63 | $start = microtime(true); 64 | 65 | $this->input = $input; 66 | $this->output = $output; 67 | 68 | $this->output->writeln('Dumping Exiftool Dictionnary... '); 69 | 70 | $logger = new Logger('Builder'); 71 | if ($output->isDebug()) { 72 | $logger->pushHandler(new StreamHandler('php://stdout')); 73 | } 74 | else { 75 | $logger->pushHandler( new NullHandler()); 76 | } 77 | 78 | $options = []; 79 | if ($input->getOption('with-mwg')) { 80 | $options[] = InformationDumper::LISTOPTION_MWG; 81 | } 82 | 83 | $path = realpath($input->getOption('path')); 84 | if($path === false) { 85 | throw new Exception(sprintf('Path "%s" does not exists.', $input->getOption('path'))); 86 | } 87 | $subPath = $path . '/' . PHPExiftool::SUBDIR; // security : do NOT rm passed cli option 88 | @mkdir($subPath, 0755, true); 89 | $logger->info(sprintf('Erasing previous files "%s/*" ', $subPath)); 90 | try { 91 | $cmd = 'rm -Rf ' . $subPath . '/* 2> /dev/null'; 92 | $output = []; 93 | @exec($cmd, $output); 94 | } 95 | catch (\Exception $e) { 96 | // no-op 97 | } 98 | $logger->info('Generating classes... '); 99 | 100 | $PHPExiftool = new PHPExiftool($path, $logger); 101 | $nClasses = $PHPExiftool->generateClasses($options, $input->getOption('lng')); 102 | 103 | $this->output->writeln( 104 | sprintf( 105 | 'Generated %d classes in %d seconds (%d Mb)', 106 | $nClasses, 107 | (microtime(true) - $start), 108 | memory_get_peak_usage() >> 20 109 | ) 110 | ); 111 | 112 | return 0; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /lib/PHPExiftool/PHPExiftool.php: -------------------------------------------------------------------------------- 1 | setClassesRootDirectory($classesRootDirectory); 25 | } 26 | if($logger === null) { 27 | $logger = new NullLogger(); 28 | } 29 | $this->setLogger($logger); 30 | $this->factory = new Factory($this); 31 | } 32 | 33 | 34 | 35 | public function setLogger(LoggerInterface $logger) 36 | { 37 | $this->logger = $logger; 38 | } 39 | 40 | /** 41 | * @param string|null $classesRootDirectory 42 | */ 43 | public function setClassesRootDirectory(?string $classesRootDirectory): void 44 | { 45 | $c = substr($classesRootDirectory, 0, 1); 46 | if ($c !== '/') { 47 | throw new DirectoryNotFoundException(sprintf("classesRootDirectory must be absolute, \"%s\" given", $classesRootDirectory)); 48 | } 49 | if (!file_exists($classesRootDirectory) || !is_writable($classesRootDirectory)) { 50 | throw new DirectoryNotFoundException(sprintf("classesRootDirectory \"%s\" must exists and be writable", $classesRootDirectory)); 51 | } 52 | 53 | $this->classesRootDirectory = realpath($classesRootDirectory); 54 | } 55 | 56 | public function generateClasses(array $options, array $lngs = ['en'], ?callable $cb = null): int 57 | { 58 | $classesDirectory = $this->classesRootDirectory . '/' . self::SUBDIR; 59 | $rootNamespace = self::ROOT_NAMESPACE . '\\' . self::SUBDIR; 60 | 61 | $dumper = new InformationDumper(new Exiftool($this->logger)); 62 | 63 | $group_ids = []; 64 | $dumper->dumpClasses( 65 | $options, 66 | $lngs, 67 | $cb ?: function (string $fq_classname, tagGroupBuilder $tgb) use($classesDirectory, &$group_ids) { 68 | $tgb->write($classesDirectory); 69 | $group_ids[$tgb->getProperty('id')] = $fq_classname; 70 | } 71 | ); 72 | 73 | if(!$cb) { 74 | $this->logger->info(sprintf("Writing helper Table")); 75 | ksort($group_ids, SORT_NATURAL + SORT_FLAG_CASE); 76 | $file = $classesDirectory . '/Helper.php'; 77 | file_put_contents($file, 78 | "classesRootDirectory . '/' . self::SUBDIR . "/Helper.php"; 96 | return file_exists($p); 97 | } 98 | 99 | /** 100 | * @return Factory 101 | */ 102 | public function getFactory(): Factory 103 | { 104 | return $this->factory; 105 | } 106 | 107 | /** 108 | * @return LoggerInterface|NullLogger|null 109 | */ 110 | public function getLogger() 111 | { 112 | return $this->logger; 113 | } 114 | 115 | /** 116 | * @return string 117 | */ 118 | public function getClassesRootDirectory(): string 119 | { 120 | return $this->classesRootDirectory; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Exiftool.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool; 13 | 14 | use Exception; 15 | use PHPExiftool\Exception\RuntimeException; 16 | use Psr\Log\LoggerAwareInterface; 17 | use Psr\Log\LoggerInterface; 18 | use Symfony\Component\Process\Process; 19 | 20 | class Exiftool implements LoggerAwareInterface 21 | { 22 | protected LoggerInterface $logger; 23 | protected ?string $binaryPath = null; 24 | 25 | public function __construct(LoggerInterface $logger, ?string $binaryPath = null) 26 | { 27 | $this->logger = $logger; 28 | 29 | $this->binaryPath = $this->getExecutableBinary($binaryPath); // ensure executable (find default if $binaryPath === null) 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function setLogger(LoggerInterface $logger): void 36 | { 37 | $this->logger = $logger; 38 | } 39 | 40 | 41 | /** 42 | * Execute a command and return the output 43 | * 44 | * @param array $command arguments without executable 45 | * @param int $timeout 46 | * @return string 47 | * @throws Exception 48 | */ 49 | public function executeCommand(array $command, int $timeout = 60): string 50 | { 51 | array_unshift($command, $this->binaryPath); 52 | $process = new Process($command); 53 | $process->setTimeout($timeout); 54 | 55 | $this->logger->info(sprintf('Exiftool executes command %s', $process->getCommandLine())); 56 | 57 | $exitcode = $process->run(); 58 | 59 | $this->logger->info(sprintf('Exiftool process returned exitcode = %s', $exitcode)); 60 | 61 | if ( ! $process->isSuccessful()) { 62 | throw new RuntimeException(sprintf("Command \"%s\"\n%s\nfailed : \"%s\", exitcode %s", join(' ', $command), var_export($command, true), $process->getErrorOutput(), $process->getExitCode())); 63 | } 64 | 65 | $output = $process->getOutput(); 66 | 67 | $this->logger->debug(sprintf("Exiftool output :\n%s", $output)); 68 | 69 | unset($process); 70 | 71 | return $output; 72 | } 73 | 74 | private function getExecutableBinary(?string $binaryPath = null): string 75 | { 76 | if(!$binaryPath) { 77 | $binaryPath = self::searchBinary($this->logger); 78 | } 79 | if (is_executable($binaryPath)) { 80 | $this->logger->debug(sprintf("Exiftool binary : \"%s\" is executable", $binaryPath)); 81 | return $binaryPath; 82 | } 83 | else { 84 | throw new RuntimeException(sprintf("Exiftool binary : \"%s\" is not executable", $binaryPath)); 85 | } 86 | } 87 | 88 | /** 89 | * 90 | * @param LoggerInterface|null $logger 91 | * @return string 92 | */ 93 | protected static function searchBinary(?LoggerInterface $logger = null): string 94 | { 95 | static $binary = null; 96 | 97 | if ($binary) { 98 | return $binary; 99 | } 100 | 101 | $testLocations = [ 102 | $dev = __DIR__ . '/../../vendor/exiftool/exiftool/exiftool', 103 | $packaged = __DIR__ . '/../../../../exiftool/exiftool/exiftool' 104 | ]; 105 | 106 | foreach ($testLocations as $i => $location) { 107 | 108 | if (defined('PHP_WINDOWS_VERSION_BUILD')) { 109 | $location .= '.exe'; 110 | } 111 | 112 | if($logger) { 113 | $logger->debug(sprintf("Searching exiftool binary as \"%s\" ...", $location)); 114 | } 115 | 116 | $rp = realpath($location); 117 | if($rp) { 118 | if($logger) { 119 | $logger->debug(sprintf(" -> checking realpath \"%s\"", $rp)); 120 | } 121 | if(is_executable($rp)) { 122 | if ($logger) { 123 | $logger->debug(sprintf(" -> -> \"%s\" is executable", $rp)); 124 | return $binary = $rp; 125 | } 126 | } 127 | else { 128 | if ($logger) { 129 | $logger->debug(sprintf(" -> -> \"%s\" is not executable %s", $rp, $i < count($testLocations) ? ", check next" : "")); 130 | } 131 | } 132 | } 133 | else { 134 | if($logger) { 135 | $logger->debug(sprintf(" -> realpath(\"%s\") = %s", $location, var_export($rp, true))); 136 | } 137 | } 138 | } 139 | 140 | throw new RuntimeException('Unable to find exiftool binary'); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/files/multiplefile.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 19 | 8.88 20 | phpunit-no-cc.xml.dist 21 | . 22 | 645 bytes 23 | 2012:04:17 21:41:50+02:00 24 | rw-r--r-- 25 | XML 26 | application/xml 27 | false 28 | false 29 | true 30 | true 31 | true 32 | true 33 | false 34 | false 35 | true 36 | false 37 | bootstrap.php 38 | display_errors 39 | on 40 | PHPExiftool Tests Suite 41 | tests 42 | 43 | 44 | 50 | 8.88 51 | phpunit.xml.dist 52 | . 53 | 1051 bytes 54 | 2012:04:18 23:25:03+02:00 55 | rw-r--r-- 56 | XML 57 | application/xml 58 | false 59 | false 60 | true 61 | true 62 | true 63 | true 64 | false 65 | false 66 | true 67 | false 68 | bootstrap.php 69 | coverage-html 70 | tests/phpunit_report/report 71 | UTF-8 72 | true 73 | false 74 | 35 75 | 70 76 | display_errors 77 | on 78 | PHPExiftool Tests Suite 79 | tests 80 | vendor 81 | tests 82 | lib/PHPExiftool/Driver/Tag/* 83 | 84 | 85 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/RDFParserTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool; 12 | 13 | use Monolog\Handler\NullHandler; 14 | use Monolog\Logger; 15 | use PHPExiftool\RDFParser; 16 | use PHPExiftool\Exception\LogicException; 17 | use PHPExiftool\Exception\ParseErrorException; 18 | use PHPExiftool\Exception\RuntimeException; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | 22 | class RDFParserTest extends TestCase 23 | { 24 | protected RDFParser $object; 25 | protected Logger $logger; 26 | 27 | protected function setUp(): void 28 | { 29 | $this->logger = new Logger('Tests'); 30 | $this->logger->pushHandler(new NullHandler()); 31 | 32 | $this->object = new RDFParser("/tmp", $this->logger); 33 | } 34 | 35 | /** 36 | * @covers RDFParser::open 37 | */ 38 | public function testOpen() 39 | { 40 | $this->object->open(file_get_contents(__DIR__ . '/../../files/simplefile.xml')); 41 | } 42 | 43 | /** 44 | * @covers RDFParser::close 45 | */ 46 | public function testClose() 47 | { 48 | $this->object->close(); 49 | } 50 | 51 | /** 52 | * @covers RDFParser::ParseEntities 53 | * @covers RDFParser::getDom 54 | * @covers RDFParser::getDomXpath 55 | * @covers RDFParser::getNamespacesFromXml 56 | */ 57 | public function testParseEntities() 58 | { 59 | $entities = $this->object 60 | ->open(file_get_contents(__DIR__ . '/../../files/simplefile.xml')) 61 | ->parseEntities(); 62 | 63 | $this->assertInstanceOf('\\Doctrine\\Common\\Collections\\ArrayCollection', $entities); 64 | $this->assertEquals(1, count($entities)); 65 | $this->assertInstanceOf('\\PHPExiftool\\FileEntity', $entities->first()); 66 | } 67 | 68 | /** 69 | * @covers RDFParser::ParseEntities 70 | * @covers RDFParser::getDom 71 | * @covers RDFParser::getDomXpath 72 | * @covers \PHPExiftool\Exception\LogicException 73 | */ 74 | public function testParseEntitiesWithoutDom() 75 | { 76 | $this->expectException(\LogicException::class); 77 | $this->object->parseEntities(); 78 | } 79 | 80 | /** 81 | * @covers RDFParser::ParseEntities 82 | * @covers RDFParser::getDom 83 | * @covers RDFParser::getDomXpath 84 | * @covers \PHPExiftool\Exception\ParseErrorException 85 | * @covers \PHPExiftool\Exception\RuntimeException 86 | */ 87 | public function testParseEntitiesWrongDom() 88 | { 89 | $this->expectException(\RuntimeException::class); 90 | $this->object->open('wrong xml')->parseEntities(); 91 | } 92 | 93 | /** 94 | * @covers RDFParser::ParseMetadatas 95 | * @covers RDFParser::getDom 96 | * @covers RDFParser::getDomXpath 97 | */ 98 | public function testParseMetadatas() 99 | { 100 | $metadatas = $this->object 101 | ->open(file_get_contents(__DIR__ . '/../../files/ExifTool.xml')) 102 | ->ParseMetadatas(); 103 | 104 | $this->assertInstanceOf('\\PHPExiftool\\Driver\\Metadata\\MetadataBag', $metadatas); 105 | $this->assertEquals(348, count($metadatas)); 106 | } 107 | 108 | /** 109 | * @covers RDFParser::Query 110 | * @covers RDFParser::readNodeValue 111 | */ 112 | public function testQuery() 113 | { 114 | $xml = " 115 | 116 | 117 | Hello World ! 118 | SGVsbG8gYmFzZTY0ICE= 119 | 120 | 121 | romain 122 | neutron 123 | 124 | 125 | 126 | "; 127 | 128 | $this->object->open($xml); 129 | 130 | $metadata_simple = $this->object->Query('NeutronSpace:SpecialRomain'); 131 | $metadata_base64 = $this->object->Query('NeutronSpace:SpecialRomainbase64'); 132 | $metadata_multi = $this->object->Query('NeutronSpace:Multi'); 133 | $null_datas = $this->object->Query('NeutronSpace:NoData'); 134 | $null_datas_2 = $this->object->Query('NamespaceUnknown:NoData'); 135 | 136 | $this->assertNull($null_datas); 137 | $this->assertNull($null_datas_2); 138 | 139 | $this->assertInstanceOf('\\PHPExiftool\\Driver\\Value\\Mono', $metadata_simple); 140 | $this->assertInstanceOf('\\PHPExiftool\\Driver\\Value\\Binary', $metadata_base64); 141 | $this->assertInstanceOf('\\PHPExiftool\\Driver\\Value\\Multi', $metadata_multi); 142 | 143 | $this->assertEquals('Hello World !', $metadata_simple->asString()); 144 | $this->assertEquals('Hello base64 !', $metadata_base64->asString()); 145 | $this->assertEquals(array('romain', 'neutron'), $metadata_multi->asArray()); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/PHPExiftool/ClassUtils/tagGroupBuilder.php: -------------------------------------------------------------------------------- 1 | tags[] = ['/**/' => $tagComments, '' => $tagProperties]; 23 | } 24 | 25 | /** 26 | * reconciliate "desc" attribute : for the same lng, keep the longuest one 27 | * 28 | * @param array $descriptions 29 | */ 30 | public function setDescription(array $descriptions) 31 | { 32 | foreach ($descriptions as $lng => $description) { 33 | if(!array_key_exists($lng, $this->descriptions)) { 34 | $this->descriptions[$lng] = $description; 35 | } 36 | else { 37 | if($description !== $this->descriptions[$lng]) { 38 | $this->logger->alert(sprintf("Conflicting description for group \"%s:%s\" : [%s] \"%s\" != \"%s\"", 39 | $this->namespace, $this->classname, 40 | $lng, $description, $this->descriptions[$lng] 41 | )); 42 | if(strlen($description) > strlen($this->descriptions[$lng])) { 43 | // arbitrary choice : keep the longest description 44 | $this->descriptions[$lng] = $description; 45 | } 46 | $this->logger->alert(sprintf(" (keeping \"%s\")", $this->descriptions[$lng])); 47 | } 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * reconciliate type -> php_type 54 | * 55 | * @param string $type 56 | * @param string|null $php_type 57 | */ 58 | public function setType(string $type, ?string $php_type) 59 | { 60 | if(!is_null($php_type)) { 61 | if($this->php_type === '') { 62 | $this->php_type = $php_type; 63 | } 64 | if($php_type != $this->php_type) { 65 | if($this->php_type) { // do no report same conflict 66 | $this->logger->alert(sprintf("Conflicting php types for group \"%s:%s\" : \"%s\" (php:%s) != \"%s\" (php:%s)", 67 | $this->namespace, $this->classname, 68 | $type, $php_type, $this->type, $this->php_type 69 | )); 70 | } 71 | $this->php_type = null; // mixed 72 | } 73 | } 74 | if($this->type === '') { 75 | $this->type = $type; 76 | } 77 | else { 78 | if($type != $this->type) { 79 | $this->type = null; // mixed 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * reconciliate "writable" flag (optimistic : one writable tag sets the taggroup as writable) 86 | * @param bool $writable 87 | */ 88 | public function setWritable(bool $writable) 89 | { 90 | if($this->writable === -1) { 91 | $this->writable = $writable ? 1 : 0; 92 | } 93 | if(($writable ? 1 : 0) !== $this->writable) { 94 | $this->logger->alert(sprintf("Conflicting 'writable' attr for group \"%s:%s\"", 95 | $this->namespace, $this->classname 96 | )); 97 | } 98 | if($writable) { 99 | $this->writable = 1; 100 | } 101 | } 102 | 103 | public function getFlags(): int 104 | { 105 | return array_key_exists('flags', $this->properties) ? $this->properties["flags"] : 0; 106 | } 107 | 108 | /** 109 | * reconciliate "count" attribute : keep value if same for all tags 110 | * @param int $count 111 | */ 112 | public function setCount(int $count) 113 | { 114 | if($this->count === -1) { 115 | $this->count = $count; 116 | } 117 | 118 | if($count !== $this->count) { 119 | if(!is_null($this->count)) { 120 | $this->logger->alert(sprintf("Conflicting 'count' attr for group \"%s:%s\"", 121 | $this->namespace, $this->classname 122 | )); 123 | } 124 | $this->count = null; 125 | } 126 | 127 | } 128 | 129 | /** 130 | * reconciliate flags : a flag must be set on all tags to be set on taggroup 131 | */ 132 | public function setFlags(array $flags) 133 | { 134 | foreach ($flags as $flag) { // "avoid", "binary", "permanent", ... 135 | if(!array_key_exists($flag, $this->flags)) { 136 | $this->flags[$flag] = 0; 137 | } 138 | $this->flags[$flag]++; 139 | } 140 | } 141 | 142 | public function isWritable(): bool 143 | { 144 | return ($this->writable === 1); 145 | } 146 | 147 | public function getCount(): int 148 | { 149 | return is_null($this->count) ? 0 : max(0, $this->count); 150 | } 151 | 152 | public function getDescriptions() 153 | { 154 | return $this->descriptions; 155 | } 156 | 157 | 158 | 159 | public function computeProperties(): Builder 160 | { 161 | $this->properties['phpType'] = $this->php_type ?: "mixed"; 162 | $this->properties['isWritable'] = $this->isWritable(); 163 | $this->properties['description'] = $this->descriptions; 164 | $this->properties['tags'] = $this->tags; 165 | if(!is_null($this->count)) { // 0 = default parent value, don't write in child 166 | $this->properties['count'] = $this->getCount(); 167 | } 168 | // flags 169 | $binFlags = 0; 170 | foreach ($this->flags as $flagName => $nOccurences) { 171 | if($nOccurences === count($this->tags)) { 172 | $name = "FLAG_" . strtoupper($flagName); 173 | $fqName = AbstractTagGroup::class . '::' . $name; 174 | if(defined($fqName)) { 175 | $binFlags |= constant($fqName); 176 | } 177 | } 178 | } 179 | // also add "writable" as a flag 180 | if($this->isWritable()) { 181 | $binFlags |= AbstractTagGroup::FLAG_WRITABLE; 182 | } 183 | if($binFlags !== 0) { // 0 is the default parent value 184 | $this->properties["flags"] = $binFlags; 185 | } 186 | 187 | return $this; 188 | } 189 | 190 | public function write(string $path): Builder 191 | { 192 | parent::write($path); 193 | return $this; 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /lib/PHPExiftool/RDFParser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool; 13 | 14 | use DOMDocument; 15 | use DOMElement; 16 | use DOMNode; 17 | use DOMNodeList; 18 | use DOMXPath; 19 | use PHPExiftool\Driver\TagGroupInterface; 20 | use PHPExiftool\Driver\TagGroupFactory; 21 | use PHPExiftool\Driver\Metadata\Metadata; 22 | use PHPExiftool\Driver\Metadata\MetadataBag; 23 | use PHPExiftool\Driver\Value\Binary; 24 | use PHPExiftool\Driver\Value\Mono; 25 | use PHPExiftool\Driver\Value\Multi; 26 | use PHPExiftool\Driver\Value\ValueInterface; 27 | use PHPExiftool\Exception\LogicException; 28 | use PHPExiftool\Exception\ParseErrorException; 29 | use PHPExiftool\Exception\RuntimeException; 30 | use PHPExiftool\Exception\TagUnknown; 31 | use Doctrine\Common\Collections\ArrayCollection; 32 | use Psr\Log\LoggerInterface; 33 | 34 | /** 35 | * Exiftool RDF output Parser 36 | * 37 | * @author Romain Neutron 38 | */ 39 | class RDFParser 40 | { 41 | /** 42 | * RDF Namespace 43 | */ 44 | const RDF_NAMESPACE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; 45 | 46 | protected ?string $XML = null; 47 | protected ?DOMDocument $DOM = null; 48 | protected ?DOMXPath $DOMXpath = null; 49 | protected array $registeredPrefixes = []; 50 | private LoggerInterface $logger; 51 | private string $classesRootDirectory; 52 | 53 | public function __construct(string $classesRootDirectory, LoggerInterface $logger) 54 | { 55 | $this->logger = $logger; 56 | $this->classesRootDirectory = $classesRootDirectory; 57 | } 58 | 59 | public function __destruct() 60 | { 61 | $this->close(); 62 | } 63 | 64 | /** 65 | * Opens an XML file for parsing 66 | * 67 | * @param string $XML 68 | * @return RDFParser 69 | */ 70 | public function open(string $XML): self 71 | { 72 | $this->close(); 73 | 74 | $this->XML = $XML; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Close the current opened XML file and reset internals 81 | * 82 | * @return RDFParser 83 | */ 84 | public function close(): self 85 | { 86 | $this->XML = null; 87 | $this->DOMXpath = null; 88 | $this->DOM = null; 89 | $this->registeredPrefixes = []; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Parse an XML string and returns an ArrayCollection of FileEntity 96 | * 97 | * @return ArrayCollection 98 | * @throws ParseErrorException 99 | */ 100 | public function ParseEntities(): ArrayCollection 101 | { 102 | /** 103 | * A default Exiftool XML can contains many RDF Descriptions 104 | */ 105 | $Entities = new ArrayCollection(); 106 | 107 | $this->logger->debug(sprintf("ParseEntities() :: searching '/rdf:RDF/rdf:Description' ...")); 108 | /** @var DOMNode $node */ 109 | foreach ($this->getDomXpath()->query('/rdf:RDF/rdf:Description') as $node) { 110 | $this->logger->debug(sprintf(" -> found node \"%s\" line %d", $node->nodeName, $node->getLineNo())); 111 | 112 | /** 113 | * Let's create a DOMDocument containing a single RDF result 114 | */ 115 | $Dom = new DOMDocument(); 116 | 117 | $DomRootElement = $Dom->createElementNS(self::RDF_NAMESPACE, 'rdf:RDF'); 118 | $DomRootElement->appendChild($Dom->importNode($node, true)); 119 | 120 | $Dom->appendChild($DomRootElement); 121 | 122 | $LocalXpath = new DOMXPath($Dom); 123 | $LocalXpath->registerNamespace('rdf', self::RDF_NAMESPACE); 124 | 125 | 126 | $RDFDescriptionRoot = $LocalXpath->query('/rdf:RDF/rdf:Description'); 127 | 128 | /** 129 | * Let's associate a Description to the corresponding file 130 | */ 131 | /** @var DOMElement $node */ 132 | $node = $RDFDescriptionRoot->item(0); 133 | $file = $node->getAttribute('rdf:about'); 134 | 135 | $Entities->set($file, new FileEntity($file, $Dom, new static($this->classesRootDirectory, $this->logger))); 136 | 137 | $this->logger->debug(sprintf(" -> new dom node \"%s\" line %d associated to file \"%s\"", $node->nodeName, $node->getLineNo(), $file)); 138 | } 139 | 140 | return $Entities; 141 | } 142 | 143 | /** 144 | * Parse an Entity associated DOM, returns the metadatas 145 | * 146 | * @return MetadataBag 147 | * @throws TagUnknown|ParseErrorException 148 | */ 149 | public function ParseMetadatas(): MetadataBag 150 | { 151 | $this->logger->debug(sprintf("ParseMetadatas() :: searching '/rdf:RDF/rdf:Description/*' ...")); 152 | 153 | $nodes = $this->getDomXpath()->query('/rdf:RDF/rdf:Description/*'); 154 | 155 | $metadatas = new MetadataBag(); 156 | 157 | /** @var DOMNode $node */ 158 | foreach ($nodes as $node) { 159 | 160 | $tagname = $this->normalize($node->nodeName); 161 | 162 | $this->logger->debug(sprintf(" -> found node \"%s\" line %d -> tagname = \"%s\"", $node->nodeName, $node->getLineNo(), $tagname)); 163 | 164 | try { 165 | $tagGroup = TagGroupFactory::getFromRDFTagname($this->classesRootDirectory, $tagname, $this->logger); 166 | $this->logger->debug(sprintf(" -> tagGroup class = \"%s\"", get_class($tagGroup))); 167 | } 168 | catch (TagUnknown $e) { 169 | $this->logger->debug(sprintf(" -> \"%s\", ignored", $e->getMessage())); 170 | continue; 171 | } 172 | 173 | $metaValue = $this->readNodeValue($node, $tagGroup); 174 | 175 | $metadata = new Metadata($tagGroup, $metaValue); 176 | 177 | $metadatas->set($tagname, $metadata); 178 | } 179 | 180 | return $metadatas; 181 | } 182 | 183 | /** 184 | * Returns the first result for a user defined query against the RDF 185 | * 186 | * @param string $query 187 | * @return ?ValueInterface The value 188 | * @throws TagUnknown|ParseErrorException 189 | */ 190 | public function Query(string $query): ?ValueInterface 191 | { 192 | $QueryParts = explode(':', $query); 193 | 194 | $DomXpath = $this->getDomXpath(); 195 | 196 | if (!in_array($QueryParts[0], $this->registeredPrefixes)) { 197 | return null; 198 | } 199 | 200 | $nodes = $DomXpath->query('/rdf:RDF/rdf:Description/' . $query); 201 | 202 | if ($nodes instanceof DOMNodeList && $nodes->length > 0) { 203 | /** @var DOMElement $node */ 204 | $node = $nodes->item(0); 205 | return $this->readNodeValue($node); 206 | } 207 | 208 | return null; 209 | } 210 | 211 | /** 212 | * Normalize a tagname based on namespaces redirections 213 | * 214 | * @param string $tagname The tagname to normalize 215 | * @return string The normalized tagname 216 | */ 217 | protected function normalize(string $tagname): string 218 | { 219 | static $namespacesRedirection = [ 220 | 'CIFF' => ['Canon', 'CanonRaw'], 221 | ]; 222 | 223 | foreach ($namespacesRedirection as $from => $to) { 224 | if (strpos($tagname, $from . ':') !== 0) { 225 | continue; 226 | } 227 | 228 | foreach ((array)$to as $substit) { 229 | $supposedTagname = str_replace($from . ':', $substit . ':', $tagname); 230 | 231 | if (TagGroupFactory::hasFromRDFTagname($this->classesRootDirectory, $supposedTagname, $this->logger)) { 232 | return $supposedTagname; 233 | } 234 | } 235 | } 236 | 237 | return $tagname; 238 | } 239 | 240 | /** 241 | * Extract all XML namespaces declared in a XML 242 | * 243 | * @param DOMDocument $dom 244 | * @return array The namespaces declared in XML 245 | */ 246 | protected static function getNamespacesFromXml(DOMDocument $dom): array 247 | { 248 | $namespaces = []; 249 | 250 | $XML = $dom->saveXML(); 251 | 252 | $pattern = "(xmlns:([a-zA-Z-_0-9]+)=['|\"]{1}(https?:[/{2,4}|\\{2,4}][\w:#%/;$()~_?/\-=\\\.&]*)['|\"]{1})"; 253 | 254 | preg_match_all($pattern, $XML, $matches, PREG_PATTERN_ORDER); 255 | 256 | foreach ($matches[2] as $key => $value) { 257 | $namespaces[$matches[1][$key]] = $value; 258 | } 259 | 260 | return $namespaces; 261 | } 262 | 263 | /** 264 | * Read the node value, decode it if needed 265 | * 266 | * @param DOMElement $node The node to read 267 | * @param ?TagGroupInterface $tag The tag associated 268 | * @return ValueInterface The value extracted 269 | * @throws TagUnknown 270 | */ 271 | 272 | // -1.1 273 | // 274 | protected function readNodeValue(DOMElement $node, ?TagGroupInterface $tagGroup = null) 275 | { 276 | $nodeName = $this->normalize($node->nodeName); 277 | 278 | if (is_null($tagGroup) && TagGroupFactory::hasFromRDFTagname($this->classesRootDirectory, $nodeName, $this->logger)) { 279 | $tagGroup = TagGroupFactory::getFromRDFTagname($this->classesRootDirectory, $nodeName, $this->logger); 280 | } 281 | 282 | if ($node->getElementsByTagNameNS(self::RDF_NAMESPACE, 'Bag')->length > 0) { 283 | 284 | $ret = []; 285 | 286 | foreach ($node->getElementsByTagNameNS(self::RDF_NAMESPACE, 'li') as $nodeElement) { 287 | $ret[] = $nodeElement->nodeValue; 288 | } 289 | 290 | if (is_null($tagGroup) || $tagGroup->isMulti()) { 291 | return new Multi($ret); 292 | } 293 | else { 294 | return new Mono(implode(' ', $ret)); 295 | } 296 | } 297 | elseif ($node->getAttribute('rdf:datatype') === 'http://www.w3.org/2001/XMLSchema#base64Binary') { 298 | 299 | if (is_null($tagGroup) || $tagGroup->isBinary()) { 300 | return Binary::loadFromBase64(trim($node->nodeValue)); 301 | } 302 | else { 303 | return new Mono(base64_decode(trim($node->nodeValue))); 304 | } 305 | } 306 | else { 307 | 308 | if (!is_null($tagGroup) && $tagGroup->isMulti()) { 309 | return new Multi($node->nodeValue); 310 | } 311 | else { 312 | return new Mono($node->nodeValue); 313 | } 314 | } 315 | } 316 | 317 | /** 318 | * Compute the DOMDocument from the XML 319 | * 320 | * @return ?DOMDocument 321 | * @throws LogicException 322 | * @throws ParseErrorException 323 | */ 324 | protected function getDom(): ?DOMDocument 325 | { 326 | if (!$this->XML) { 327 | throw new LogicException('You must open an XML first'); 328 | } 329 | 330 | if (!$this->DOM) { 331 | 332 | $this->DOM = new DOMDocument; 333 | 334 | /** 335 | * We shut up the warning to exclude an exception in case Warnings are 336 | * transformed in exception 337 | */ 338 | if (!@$this->DOM->loadXML($this->XML)) { 339 | throw new ParseErrorException('Unable to load XML'); 340 | } 341 | } 342 | 343 | return $this->DOM; 344 | } 345 | 346 | /** 347 | * Compute the DOMXpath from the DOMDocument 348 | * 349 | * @return ?DOMXpath The DOMXpath object related to the XML 350 | * @throws RuntimeException|ParseErrorException 351 | */ 352 | protected function getDomXpath(): ?DOMXPath 353 | { 354 | if (!$this->DOMXpath) { 355 | try { 356 | $this->DOMXpath = new DOMXPath($this->getDom()); 357 | } 358 | catch (ParseErrorException $e) { 359 | throw new RuntimeException('Unable to parse the XML'); 360 | } 361 | 362 | $this->DOMXpath->registerNamespace('rdf', self::RDF_NAMESPACE); 363 | 364 | foreach (static::getNamespacesFromXml($this->getDom()) as $prefix => $uri) { 365 | $this->registeredPrefixes = array_merge($this->registeredPrefixes, (array)$prefix); 366 | $this->DOMXpath->registerNamespace($prefix, $uri); 367 | } 368 | } 369 | 370 | return $this->DOMXpath; 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /lib/PHPExiftool/ClassUtils/Builder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool\ClassUtils; 13 | 14 | use Exception; 15 | use InvalidArgumentException; 16 | use Psr\Log\LoggerInterface; 17 | use Psr\Log\NullLogger; 18 | use ReflectionClass; 19 | use ReflectionProperty; 20 | 21 | /** 22 | * Build and write TagGroup classes 23 | * 24 | * @author Romain Neutron - imprec@gmail.com 25 | * @license http://opensource.org/licenses/MIT MIT 26 | */ 27 | class Builder 28 | { 29 | protected string $license = '/* 30 | * This file is part of the PHPExifTool package. 31 | * 32 | * (c) Alchemy 33 | * 34 | * For the full copyright and license information, please view the LICENSE 35 | * file that was distributed with this source code. 36 | */'; 37 | // protected int $xmlLine = 0; 38 | // protected array $duplicateXmlLines = []; 39 | // protected array $conflictingXmlLines = []; 40 | protected string $namespace = ""; 41 | protected string $classname = ""; 42 | protected array $consts = []; 43 | protected array $properties = []; 44 | protected $extends; 45 | protected array $uses = []; 46 | protected array $classAnnotations = []; 47 | protected LoggerInterface $logger; 48 | 49 | static ?ReflectionClass $reflectionClass = null; 50 | private string $rootNamespace; 51 | 52 | /** 53 | * @throws Exception 54 | */ 55 | public function __construct(string $rootNamespace, string $namespace, string $classname, array $consts, array $properties, $extends = null, array $uses = [], array $classAnnotations = []) 56 | { 57 | // singleton 58 | if (is_null(self::$reflectionClass) && $extends) { 59 | self::$reflectionClass = new ReflectionClass("PHPExiftool\\Driver\\" . $extends); 60 | } 61 | 62 | $namespace = trim($namespace, '\\'); 63 | 64 | foreach (explode('\\', $namespace) as $piece) { 65 | if ($piece == '') { 66 | continue; 67 | } 68 | 69 | if (!$this->checkPHPVarName($piece)) { 70 | throw new Exception(sprintf('Invalid namespace %s', $namespace)); 71 | } 72 | } 73 | if (!$this->checkPHPVarName($classname)) { 74 | throw new Exception(sprintf('Invalid namespace %s', $namespace)); 75 | } 76 | 77 | // $this->xmlLine = $xmlLine; 78 | $this->rootNamespace = $rootNamespace; 79 | $this->namespace = $namespace; 80 | $this->classname = $classname; 81 | $this->properties = $properties; 82 | $this->consts = $consts; 83 | $this->extends = $extends; 84 | $this->uses = $uses; 85 | $this->classAnnotations = $classAnnotations; 86 | 87 | $this->logger = new NullLogger(); 88 | 89 | return $this; 90 | } 91 | 92 | public function setLogger(LoggerInterface $logger) 93 | { 94 | $this->logger = $logger; 95 | 96 | return $this; 97 | } 98 | 99 | // public function addDuplicate(int $xmlLine, bool $conflicting) 100 | // { 101 | // if($conflicting) { 102 | // $this->conflictingXmlLines[] = $xmlLine; 103 | // } 104 | // else { 105 | // $this->duplicateXmlLines[] = $xmlLine; 106 | // } 107 | // } 108 | // 109 | // public function getXmlLine(): int 110 | // { 111 | // return $this->xmlLine; 112 | // } 113 | 114 | public function getNamespace(): string 115 | { 116 | return $this->namespace; 117 | } 118 | 119 | public function getClassname(): string 120 | { 121 | return $this->classname; 122 | } 123 | 124 | public function getProperty($property) 125 | { 126 | return $this->properties[$property] ?? null; 127 | } 128 | 129 | public function setProperty($property, $value) 130 | { 131 | $this->properties[$property] = $value; 132 | } 133 | 134 | public function getPathfile(string $rootPath): string 135 | { 136 | $subdir = str_replace('\\', '/', $this->namespace); 137 | @mkdir($rootPath . '/' . $subdir, 0754, true); 138 | return $rootPath . '/' . $subdir . '/' . $this->classname . '.php'; 139 | } 140 | 141 | /** 142 | * @throws Exception 143 | */ 144 | protected function write(string $rootPath): Builder 145 | { 146 | $fp = $this->getPathfile($rootPath); 147 | 148 | if (file_exists($fp)) { 149 | unlink($fp); 150 | } 151 | 152 | file_put_contents($fp, $this->generateContent($rootPath)); 153 | 154 | return $this; 155 | } 156 | 157 | public function generateContent(string $rootPath) 158 | { 159 | $content = "\n\nnamespace ".$this->rootNamespace.'\\'.$this->namespace.";\n\n"; 160 | 161 | foreach ($this->uses as $use) { 162 | $content .= "use " . ltrim($use, "\\") . ";\n"; 163 | } 164 | if ($this->uses) { 165 | $content .= "\n"; 166 | } 167 | 168 | // // add debug infos related to xml dump 169 | // if($this->xmlLine > 0) { // no line number for "type" classes 170 | // $content .= "/**\n * XML line : " . $this->xmlLine . "\n"; 171 | // if (!empty($this->duplicateXmlLines)) { 172 | // $content .= " * Duplicates: [" . join(', ', $this->duplicateXmlLines) . "]\n"; 173 | // if (!empty($this->conflictingXmlLines)) { 174 | // $content .= " * Conflictings: [" . join(', ', $this->conflictingXmlLines) . "]\n"; 175 | // } 176 | // } 177 | // $content .= " */\n"; 178 | // } 179 | 180 | 181 | if ($this->classAnnotations) { 182 | $content .= "/**\n"; 183 | foreach ($this->classAnnotations as $annotation) { 184 | $content .= " * " . $annotation . "\n"; 185 | } 186 | $content .= " */\n"; 187 | } 188 | 189 | $content .= "class "; 190 | 191 | if ($this->extends) { 192 | $content .= " extends "; 193 | } 194 | 195 | $content .= "\n{\n"; 196 | 197 | $content .= $this->generateClassConsts($this->consts); 198 | 199 | $content .= $this->generateClassProperties($this->properties); 200 | 201 | $content .= "\n}\n"; 202 | 203 | // if (!is_dir(dirname($this->getPathfile()))) { 204 | // mkdir(dirname($this->getPathfile()), 0754, true); 205 | // } 206 | 207 | return str_replace( 208 | ['', '', '', ''], 209 | [$this->license, $this->namespace, $this->classname, $this->extends], 210 | $content 211 | ); 212 | } 213 | 214 | protected function generateClassConsts(array $consts, $depth = 0): string 215 | { 216 | $buffer = ""; 217 | $space = " "; 218 | 219 | foreach ($consts as $key => $value) { 220 | $buffer .= sprintf("%sconst %s = %s;\n", $space, $key, $this->quote($value)); 221 | } 222 | 223 | return $buffer; 224 | } 225 | 226 | protected function getAttributeProperty(string $key): ?ReflectionProperty 227 | { 228 | static $attrTypes = []; 229 | 230 | if (!array_key_exists($key, $attrTypes)) { 231 | $attrTypes[$key] = null; 232 | try { 233 | $attrTypes[$key] = self::$reflectionClass->getProperty($key); 234 | } 235 | catch (Exception $e) { 236 | // no-op 237 | // throw new Exception(sprintf("Attribute \"%s\" must be defined in %s", $key, self::$reflectionClass->getName())); 238 | } 239 | } 240 | 241 | return $attrTypes[$key]; 242 | } 243 | 244 | 245 | protected function generateClassProperties(array $properties, $depth = 0): string 246 | { 247 | $buffer = ""; 248 | $space = " "; 249 | $spaces = str_repeat($space, $depth); 250 | 251 | foreach ($properties as $key => $value) { 252 | 253 | if ($key === "/**/") { 254 | // special key to be rendered as comments 255 | $buffer .= $spaces . $space . "/**\n"; 256 | foreach ($value as $k => $v) { 257 | $buffer .= sprintf("%s * %s : %s\n", $spaces . $space, $k, $v); 258 | } 259 | $buffer .= $spaces . $space . " */\n"; 260 | continue; 261 | } 262 | 263 | 264 | // switch($this->extends) { 265 | // case "AbstractTag": 266 | // $attributeType = AbstractTag::getAttributeType($key); 267 | // break; 268 | // case "AbstractType": 269 | // $attributeType = AbstractType::getAttributeType($key); 270 | // break; 271 | // default: 272 | // $attributeType = "string"; 273 | // } 274 | 275 | $visibility = 'private'; 276 | $type = ''; 277 | if (!is_null($attributeProperty = $this->getAttributeProperty($key))) { 278 | $type = $attributeProperty->getType(); 279 | $type = ($type->allowsNull() ? '?' : '') . $type->getName(); 280 | if ($attributeProperty->isPrivate()) { 281 | $visibility = 'private'; 282 | } 283 | elseif ($attributeProperty->isProtected()) { 284 | $visibility = 'protected'; 285 | } 286 | elseif ($attributeProperty->isPublic()) { 287 | $visibility = 'public'; 288 | } 289 | } 290 | 291 | if (is_array($value)) { 292 | if ($key === '') { 293 | // special case empty key : render down one level 294 | $val = $this->generateClassProperties($value, $depth); 295 | } 296 | else { 297 | $val = "[\n" . $this->generateClassProperties($value, $depth + 1); 298 | $val .= $spaces . $space . "]"; 299 | } 300 | } 301 | else { 302 | $val = $this->quote($value, $type); 303 | } 304 | if ($depth == 0) { 305 | $buffer .= sprintf("\n%s%s %s \$%s = %s;\n", 306 | $space, 307 | $visibility, 308 | $type, 309 | $key, 310 | $val 311 | ); 312 | } 313 | else { 314 | if ($key === '') { 315 | // special case empty key : render down one level 316 | $buffer .= $val; 317 | } 318 | else { 319 | $buffer .= $spaces . $space . $this->quote($key) . " => " . $val . ",\n"; 320 | } 321 | } 322 | } 323 | 324 | return $buffer; 325 | } 326 | 327 | protected function generateFlags(string $name) 328 | { 329 | 330 | } 331 | 332 | protected function checkPHPVarName($var) 333 | { 334 | return preg_match('/^[a-z]+[\\w]*$/i', $var); 335 | } 336 | 337 | protected function quote($value, $type = null): string 338 | { 339 | if ($type && $type[0] == '?') { 340 | // nullable type 341 | if (is_null($value)) { 342 | return 'null'; 343 | } 344 | else { 345 | return $this->quote($value, substr($type, 1)); 346 | } 347 | } 348 | switch ($type) { 349 | case 'string': 350 | return "'" . str_replace(['\\', '\''], ['\\\\', '\\\''], $value) . "'"; 351 | case 'bool': 352 | if (is_bool($value)) { 353 | return $value ? "true" : "false"; 354 | } 355 | if (in_array(strtolower($value), ['true', 'false'])) { 356 | return strtolower($value); 357 | } 358 | throw new InvalidArgumentException(sprintf("\"%s\" can't be converted to bool", $value)); 359 | case 'int': 360 | $data = strval(intval($value)); 361 | 362 | // Do not use PHP_INT_MAX as 32/64 bit dependant 363 | // if ($data >= -2147483648 && $data <= 2147483647) { 364 | return $data; 365 | // } 366 | // return "'" . $value . "'"; 367 | // throw new \InvalidArgumentException(sprintf("\"%s\" can't be converted to int", $value)); 368 | default: 369 | if (ctype_digit(trim($value))) { 370 | try { 371 | return $this->quote($value, 'int'); 372 | } 373 | catch (InvalidArgumentException $e) { 374 | return $this->quote($value, 'string'); 375 | } 376 | } 377 | if (in_array(strtolower($value), ['true', 'false'])) { 378 | return $this->quote($value, 'bool'); 379 | } 380 | 381 | return $this->quote($value, 'string'); 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/ReaderTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool; 12 | 13 | use PHPExiftool\Exception\EmptyCollectionException; 14 | use PHPExiftool\Exception\LogicException; 15 | use PHPExiftool\Exception\RuntimeException; 16 | use PHPExiftool\PHPExiftool; 17 | use PHPExiftool\Reader; 18 | use PHPUnit\Framework\TestCase; 19 | use Symfony\Component\Process\Process; 20 | 21 | 22 | 23 | class ReaderTest extends TestCase { 24 | 25 | private ?PHPExiftool $PHPExiftool = null; 26 | protected static string $tmpDir = ""; 27 | protected static bool $disableSymLinkTest = false; 28 | 29 | public static function setUpBeforeClass(): void 30 | { 31 | parent::setUpBeforeClass(); 32 | 33 | $tmpDir = __DIR__ . '/tmp'; 34 | 35 | if (defined('PHP_WINDOWS_VERSION_BUILD')) { 36 | $command = ['rmdir', '/q', '/s', escapeshellarg($tmpDir)]; 37 | } else { 38 | $command = ['rmdir', '-Rf', escapeshellarg($tmpDir)]; 39 | } 40 | 41 | $process = new Process($command); 42 | $process->run(); 43 | 44 | if (!is_dir($tmpDir)) { 45 | mkdir($tmpDir); 46 | } 47 | 48 | self::$tmpDir = $tmpDir . '/exiftool_reader'; 49 | 50 | if (!is_dir(self::$tmpDir)) { 51 | mkdir(self::$tmpDir); 52 | } 53 | 54 | copy(__DIR__.'/../../files/ExifTool.jpg', self::$tmpDir . '/test2.jpg'); 55 | copy(__DIR__.'/../../files/ExifTool.jpg', self::$tmpDir . '/test.jpg'); 56 | 57 | if (!is_dir(self::$tmpDir . '/dir')) { 58 | mkdir(self::$tmpDir . '/dir'); 59 | } 60 | if (!is_dir(self::$tmpDir . '/usr')) { 61 | mkdir(self::$tmpDir . '/usr'); 62 | } 63 | 64 | $tmpDir2 = $tmpDir . '/exiftool_reader2'; 65 | 66 | if (!is_dir($tmpDir2)) { 67 | mkdir($tmpDir2); 68 | } 69 | 70 | copy(__DIR__.'/../../files/ExifTool.jpg', $tmpDir2 . '/test2.jpg'); 71 | 72 | if (defined('PHP_WINDOWS_VERSION_BUILD')) { 73 | self::$disableSymLinkTest = true; 74 | } 75 | elseif (!is_link(self::$tmpDir . '/symlink')) { 76 | 77 | if (!@symlink($tmpDir2, self::$tmpDir . '/symlink')) { 78 | self::$disableSymLinkTest = true; 79 | } 80 | } 81 | 82 | copy(__DIR__.'/../../files/plop/CanonRaw.cr2', self::$tmpDir . '/dir/CanonRaw.cr2'); 83 | 84 | $tmpDir3 = $tmpDir . '/exiftool_reader3'; 85 | 86 | if (!is_dir($tmpDir3)) { 87 | mkdir($tmpDir3); 88 | } 89 | 90 | if (!is_dir($tmpDir3 . '/.svn')) { 91 | mkdir($tmpDir3 . '/.svn'); 92 | } 93 | 94 | if (!is_dir($tmpDir3 . '/.roro')) { 95 | mkdir($tmpDir3 . '/.roro'); 96 | } 97 | 98 | if (!is_dir($tmpDir3 . '/.git')) { 99 | mkdir($tmpDir3 . '/.git'); 100 | } 101 | 102 | touch($tmpDir3 . '/.git/config'); 103 | touch($tmpDir3 . '/.roro/.roro.tmp'); 104 | copy(__DIR__.'/../../files/ExifTool.jpg', $tmpDir3 . '/.exiftool.jpg'); 105 | 106 | } 107 | 108 | protected function setUp(): void 109 | { 110 | parent::setUp(); 111 | $this->PHPExiftool = new PHPExiftool("/tmp"); 112 | } 113 | 114 | private function createReader(): Reader 115 | { 116 | return $this->PHPExiftool->getFactory()->createReader(); 117 | } 118 | 119 | /** 120 | * @covers Reader::__destruct 121 | */ 122 | protected function tearDown(): void 123 | { 124 | parent::tearDown(); 125 | } 126 | 127 | /** 128 | * @covers Reader::getIterator 129 | */ 130 | public function testGetIterator() 131 | { 132 | $file = self::$tmpDir . '/test.jpg'; 133 | $reader = $this->createReader(); 134 | $this->assertInstanceOf('\\Iterator', $reader->files($file)->getIterator()); 135 | } 136 | 137 | /** 138 | * @covers Reader::append 139 | * @covers Reader::all 140 | */ 141 | public function testAppend() 142 | { 143 | $reader1 = $this->createReader(); 144 | $file1 = self::$tmpDir . '/test.jpg'; 145 | $file2 = self::$tmpDir . '/test2.jpg'; 146 | $file3 = self::$tmpDir . '/dir/CanonRaw.cr2'; 147 | $this->assertEquals(1, count($reader1->files($file1)->all())); 148 | 149 | 150 | $reader2 = $this->createReader(); 151 | $reader2->files(array($file2, $file3)); 152 | $this->assertEquals(3, count($reader2->append($reader1)->all())); 153 | } 154 | 155 | /** 156 | * @covers Reader::sort 157 | * @covers Reader::all 158 | */ 159 | public function testSort() 160 | { 161 | $file1 = self::$tmpDir . '/test.jpg'; 162 | $file2 = self::$tmpDir . '/test2.jpg'; 163 | $file3 = self::$tmpDir . '/dir/CanonRaw.cr2'; 164 | 165 | $reader = $this->createReader(); 166 | $reader->files(array($file3, $file2, $file1)); 167 | $reader->sort(array('directory', 'filename', 'cigarette')); 168 | 169 | $results = array(); 170 | 171 | foreach ($reader->all() as $entity) { 172 | $results[] = basename($entity->getFile()); 173 | } 174 | 175 | $this->assertSame(array('test.jpg', 'test2.jpg', 'CanonRaw.cr2'), $results); 176 | } 177 | 178 | /** 179 | * @covers Reader::files 180 | * @covers Reader::buildQuery 181 | * @throws EmptyCollectionException 182 | */ 183 | public function testFiles() 184 | { 185 | $reader = $this->createReader(); 186 | 187 | $file1 = self::$tmpDir . '/test.jpg'; 188 | $reader->files($file1); 189 | 190 | $file2 = $reader->files(self::$tmpDir . '/test.jpg')->first()->getFile(); 191 | 192 | $this->assertEquals(realpath($file1), realpath($file2)); 193 | } 194 | 195 | /** 196 | * @covers Reader::resetResults 197 | */ 198 | public function testResetFilters() 199 | { 200 | $reader = $this->createReader(); 201 | 202 | $file = self::$tmpDir . '/test.jpg'; 203 | $reader->files($file)->all(); 204 | $file = self::$tmpDir . '/test2.jpg'; 205 | $reader->files($file)->all(); 206 | 207 | $this->assertEquals(2, count($reader->all())); 208 | } 209 | 210 | /** 211 | * @covers Reader::ignoreDotFiles 212 | * @covers Reader::all 213 | */ 214 | public function testIgnoreVCS() 215 | { 216 | $reader = $this->createReader(); 217 | 218 | $reader->in(self::$tmpDir . '3'); 219 | $this->assertEquals(1, count($reader->all())); 220 | } 221 | 222 | /** 223 | * @covers Reader::ignoreDotFiles 224 | * @covers Reader::all 225 | */ 226 | public function testIgnoreDotFiles() 227 | { 228 | $reader = $this->createReader(); 229 | 230 | $reader->in(self::$tmpDir . '3'); 231 | $this->assertEquals(1, count($reader->all())); 232 | 233 | $reader->ignoreDotFiles()->in(self::$tmpDir . '3'); 234 | $this->assertEquals(0, count($reader->all())); 235 | } 236 | 237 | /** 238 | * @covers Reader::in 239 | * @covers Reader::buildQuery 240 | * @covers Reader::all 241 | */ 242 | public function testIn() 243 | { 244 | $reader = $this->createReader(); 245 | 246 | $reader->reset()->in(self::$tmpDir); 247 | $this->assertEquals(3, count($reader->all())); 248 | 249 | $reader->reset()->in(self::$tmpDir . '/dir'); 250 | $this->assertEquals(1, count($reader->all())); 251 | 252 | $reader->reset()->in(__DIR__ . '/../../../vendor/exiftool/exiftool/'); 253 | 254 | foreach ($reader as $file) { 255 | $m = $file->getMetadatas(); 256 | $this->assertEquals(basename($file->getFile()), $file->getMetadatas()->get('System:FileName')->getValue()->asString()); 257 | } 258 | } 259 | 260 | /** 261 | * @covers Reader::exclude 262 | * @covers Reader::computeExcludeDirs 263 | * @covers Reader::buildQuery 264 | * @covers Reader::all 265 | */ 266 | public function testExclude() 267 | { 268 | $reader = $this->createReader(); 269 | 270 | $reader 271 | ->in(self::$tmpDir) 272 | ->exclude(self::$tmpDir . '/dir'); 273 | 274 | $this->assertEquals(2, count($reader->all())); 275 | } 276 | 277 | /** 278 | * @dataProvider getExclude 279 | * @covers Reader::computeExcludeDirs 280 | * @covers Reader::all 281 | */ 282 | public function testComputeExcludeDirs($dir) 283 | { 284 | $reader = $this->createReader(); 285 | 286 | $reader 287 | ->in(self::$tmpDir) 288 | ->exclude($dir) 289 | ->all(); 290 | } 291 | 292 | public function getExclude(): array 293 | { 294 | return array( 295 | array(self::$tmpDir . '/dir/'), 296 | array(self::$tmpDir . '/dir'), 297 | array('dir'), 298 | array('/dir'), 299 | array('/usr'), 300 | array('usr'), 301 | array('dir/'), 302 | ); 303 | } 304 | 305 | /** 306 | * @dataProvider getWrongExclude 307 | * @covers Reader::computeExcludeDirs 308 | * @covers \PHPExiftool\Exception\RuntimeException 309 | */ 310 | public function testComputeExcludeDirsFail($dir) 311 | { 312 | $reader = $this->createReader(); 313 | 314 | $this->expectException(RuntimeException::class); 315 | $reader 316 | ->in(self::$tmpDir) 317 | ->exclude($dir) 318 | ->all(); 319 | } 320 | 321 | public function getWrongExclude(): array 322 | { 323 | return array( 324 | array(self::$tmpDir . '/dir/dir2'), 325 | array(self::$tmpDir . '/dirlo'), 326 | array('dir/dir2'), 327 | array('/usr/local'), 328 | array('usr/local'), 329 | array('/tmp'), 330 | ); 331 | } 332 | 333 | /** 334 | * @covers Reader::extensions 335 | * @covers Reader::buildQuery 336 | * @covers Reader::buildQueryAndExecute 337 | */ 338 | public function testExtensions() 339 | { 340 | $reader = $this->createReader(); 341 | $reader->in(self::$tmpDir); 342 | $this->assertEquals(3, count($reader->all())); 343 | 344 | $reader = $this->createReader(); 345 | $reader->in(self::$tmpDir)->notRecursive()->extensions(array('cr2')); 346 | $this->assertEquals(0, count($reader->all())); 347 | 348 | $reader = $this->createReader(); 349 | $reader->in(self::$tmpDir)->extensions(array('cr2')); 350 | $this->assertEquals(1, count($reader->all())); 351 | 352 | $reader = $this->createReader(); 353 | $reader->in(self::$tmpDir)->extensions(array('jpg')); 354 | $this->assertEquals(2, count($reader->all())); 355 | 356 | $reader = $this->createReader(); 357 | $reader->in(self::$tmpDir)->extensions('jpg')->extensions('cr2'); 358 | $this->assertEquals(3, count($reader->all())); 359 | 360 | $reader = $this->createReader(); 361 | $reader->in(self::$tmpDir)->extensions(array('jpg'), false); 362 | $this->assertEquals(1, count($reader->all())); 363 | 364 | $reader = $this->createReader(); 365 | $reader->in(self::$tmpDir)->extensions(array('cr2', 'jpg'), false)->notRecursive(); 366 | $this->assertEquals(0, count($reader->all())); 367 | } 368 | 369 | /** 370 | * @covers Reader::extensions 371 | * @covers \PHPExiftool\Exception\LogicException 372 | */ 373 | public function testExtensionsMisUse() 374 | { 375 | $reader = $this->createReader(); 376 | 377 | $this->expectException(LogicException::class); 378 | $reader->extensions('exiftool')->extensions('jpg', false); 379 | } 380 | 381 | /** 382 | * @covers Reader::followSymLinks 383 | */ 384 | public function testFollowSymLinks() 385 | { 386 | if (self::$disableSymLinkTest) { 387 | $this->markTestSkipped('This system does not support symlinks'); 388 | } 389 | 390 | $reader = $this->createReader(); 391 | 392 | $reader->in(self::$tmpDir) 393 | ->followSymLinks(); 394 | 395 | $this->assertInstanceOf('\\Doctrine\\Common\\Collections\\ArrayCollection', $reader->all()); 396 | $this->assertEquals(4, count($reader->all())); 397 | } 398 | 399 | /** 400 | * @covers Reader::notRecursive 401 | * @covers Reader::buildQuery 402 | */ 403 | public function testNotRecursive() 404 | { 405 | $reader = $this->createReader(); 406 | 407 | $reader->in(self::$tmpDir)->notRecursive(); 408 | $this->assertEquals(2, count($reader->all())); 409 | } 410 | 411 | /** 412 | * @covers Reader::getOneOrNull 413 | */ 414 | public function testGetOneOrNull() 415 | { 416 | $reader = $this->createReader(); 417 | 418 | $reader->in(self::$tmpDir)->notRecursive()->extensions(array('jpg', 'cr2'), false); 419 | 420 | $this->assertNull($reader->getOneOrNull()); 421 | } 422 | 423 | /** 424 | * @covers Reader::first 425 | * @covers \PHPExiftool\Exception\EmptyCollectionException 426 | */ 427 | public function testFirstEmpty() 428 | { 429 | $reader = $this->createReader(); 430 | 431 | $this->expectException(EmptyCollectionException::class); 432 | $reader->in(self::$tmpDir)->notRecursive()->extensions(array('jpg', 'cr2'), false); 433 | $reader->first(); 434 | } 435 | 436 | /** 437 | * @covers Reader::first 438 | */ 439 | public function testFirst() 440 | { 441 | $reader = $this->createReader(); 442 | 443 | $reader->in(self::$tmpDir); 444 | 445 | $this->assertInstanceOf('\\PHPExiftool\\FileEntity', $reader->first()); 446 | } 447 | 448 | /** 449 | * @covers Reader::buildQuery 450 | */ 451 | public function testFail() 452 | { 453 | $reader = $this->createReader(); 454 | 455 | $this->expectException(LogicException::class); 456 | $reader->all(); 457 | } 458 | 459 | /** 460 | * @covers Reader::all 461 | * @covers Reader::buildQueryAndExecute 462 | */ 463 | public function testAll() 464 | { 465 | $reader = $this->createReader(); 466 | 467 | $reader->in(self::$tmpDir); 468 | 469 | $this->assertInstanceOf('\\Doctrine\\Common\\Collections\\ArrayCollection', $reader->all()); 470 | $this->assertEquals(3, count($reader->all())); 471 | } 472 | 473 | } 474 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Writer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool; 13 | 14 | use Exception; 15 | use PHPExiftool\Driver\Metadata\Metadata; 16 | use PHPExiftool\Driver\Metadata\MetadataBag; 17 | use PHPExiftool\Exception\InvalidArgumentException; 18 | use Psr\Log\LoggerInterface; 19 | 20 | /** 21 | * Exiftool Metadatas Writer, it will be used to write metadatas in files 22 | * 23 | * Example usage : 24 | * 25 | * $Writer = new Writer(); 26 | * 27 | * $metadatas = new MetadataBag(); 28 | * $metadata->add(new Metadata(new IPTC\ObjectName(), Value\Mono('Super title !'))); 29 | * 30 | * //writes the metadatas to the file 31 | * $Writer->writes('image.jpg', $metadatas); 32 | * 33 | * //writes the metadatas to image_copied.jpg 34 | * $Writer->writes('image.jpg', $metadatas, 'image_copied.jpg'); 35 | * 36 | * @todo implement remove partial content 37 | * @todo implement binary thumbnails 38 | * @todo implements stay_open 39 | */ 40 | class Writer 41 | { 42 | const MODE_IPTC2XMP = 1; 43 | const MODE_IPTC2EXIF = 2; 44 | const MODE_EXIF2IPTC = 4; 45 | const MODE_EXIF2XMP = 8; 46 | const MODE_PDF2XMP = 16; 47 | const MODE_XMP2PDF = 32; 48 | const MODE_GPS2XMP = 64; 49 | const MODE_XMP2EXIF = 128; 50 | const MODE_XMP2IPTC = 256; 51 | const MODE_XMP2GPS = 512; 52 | const MODULE_MWG = 1; 53 | 54 | protected int $mode; 55 | protected int $modules; 56 | protected bool $erase = false; 57 | protected Exiftool $exiftool; 58 | private bool $eraseProfile = false; 59 | protected int $timeout = 60; 60 | 61 | protected function __construct(Exiftool $exiftool) 62 | { 63 | $this->exiftool = $exiftool; 64 | $this->reset(); 65 | } 66 | 67 | public static function create(Exiftool $exiftool) 68 | { 69 | return new self($exiftool); 70 | } 71 | 72 | public function setTimeout($timeout): self 73 | { 74 | $this->timeout = $timeout; 75 | 76 | return $this; 77 | } 78 | 79 | public function reset(): self 80 | { 81 | $this->mode = 0; 82 | $this->modules = 0; 83 | $this->erase = false; 84 | $this->eraseProfile = false; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Enable / Disable modes 91 | * 92 | * @param integer $mode One of the self::MODE_* 93 | * @param Boolean $active Enable or disable the mode 94 | * @return Writer 95 | */ 96 | public function setMode(int $mode, bool $active): self 97 | { 98 | if ($active) { 99 | $this->mode |= $mode; 100 | } else { 101 | $this->mode = $this->mode & ~$mode; 102 | } 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Return true if the mode is enabled 109 | * 110 | * @param integer $mode One of the self::MODE_* 111 | * @return Boolean True if the mode is enabled 112 | */ 113 | public function isMode(int $mode): bool 114 | { 115 | return (boolean) ($this->mode & $mode); 116 | } 117 | 118 | /** 119 | * Enable / disable module. 120 | * There's currently only one module self::MODULE_MWG 121 | * 122 | * @param integer $module One of the self::MODULE_* 123 | * @param Boolean $active Enable or disable the module 124 | * @return Writer 125 | */ 126 | public function setModule(int $module, bool $active): self 127 | { 128 | if ($active) { 129 | $this->modules |= $module; 130 | } else { 131 | $this->modules = $this->modules & ~$module; 132 | } 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Return true if the module is enabled 139 | * 140 | * @param integer $module 141 | * @return boolean 142 | */ 143 | public function hasModule(int $module): bool 144 | { 145 | return (boolean) ($this->modules & $module); 146 | } 147 | 148 | /** 149 | * If set to true, erase all metadatas before write 150 | * 151 | * @param Boolean $boolean Whether to erase metadata or not before writing. 152 | * @param Boolean $maintainICCProfile Whether to maintain or not ICC Profile in case of erasing metadata. 153 | */ 154 | public function erase(bool $boolean, bool $maintainICCProfile = false) 155 | { 156 | $this->erase = $boolean; 157 | $this->eraseProfile = !$maintainICCProfile; 158 | } 159 | 160 | /** 161 | * Copy metadatas from one file to another. 162 | * Both files must exist. 163 | * 164 | * @param string $file_src The input file 165 | * @param string $file_dest The input file 166 | * 167 | * @return int the number "write" operations, or null if exiftool returned nothing we understand 168 | * event for no-op (file unchanged), 1 is returned so the caller does not think the command failed. 169 | * 170 | * @throws InvalidArgumentException 171 | * @throws Exception 172 | */ 173 | public function copy(string $file_src, string $file_dest): ?int 174 | { 175 | if ( ! file_exists($file_src)) { 176 | throw new InvalidArgumentException(sprintf('src %s does not exists', $file_src)); 177 | } 178 | if ( ! file_exists($file_dest)) { 179 | throw new InvalidArgumentException(sprintf('dest %s does not exists', $file_dest)); 180 | } 181 | $command = [ 182 | '-overwrite_original_in_place', 183 | '-tagsFromFile', 184 | $file_src, 185 | $file_dest 186 | ]; 187 | $ret = $this->exiftool->executeCommand($command, $this->timeout); 188 | 189 | // exiftool may print (return) a bunch of lines, even for a single command 190 | // e.g. deleting tags of a file with NO tags may return 2 lines... 191 | // | exiftool -all:all= notags.jpg 192 | // | 0 image files updated 193 | // | 1 image files unchanged 194 | // ... which is NOT an error, 195 | // so it's not easy to decide from the output when something went REALLY wrong 196 | $n_unchanged = $n_changed = 0; 197 | foreach(explode("\n", $ret) as $line) { 198 | if (preg_match("/(\\d+) image files (copied|created|updated|unchanged)/", $line, $matches)) { 199 | if($matches[2] == 'unchanged') { 200 | $n_unchanged += (int)($matches[1]); 201 | } 202 | else { 203 | $n_changed += (int)($matches[1]); 204 | } 205 | } 206 | } 207 | // first chance, changes happened 208 | if($n_changed > 0) { 209 | // return $n_changed; 210 | return 1; // so tests are ok 211 | } 212 | // second chance, at least one no-op happened 213 | if($n_unchanged > 0) { 214 | return 1; 215 | } 216 | // too bad 217 | return null; 218 | } 219 | 220 | /** 221 | * Writes metadatas to the file. If a destination is provided, original file 222 | * is not modified. 223 | * 224 | * @param string $file The input file 225 | * @param MetadataBag $metadatas A bag of metadatas 226 | * @param string|null $destination The output file 227 | * @param array $resolutionXY The dpi resolution array(xresolution, yresolution) 228 | * 229 | * @return int the number "write" operations, or null if exiftool returned nothing we understand 230 | * even for no-op (file unchanged), 1 is returned so the caller does not think the command failed. 231 | * 232 | * @throws InvalidArgumentException|Exception 233 | */ 234 | public function write(string $file, MetadataBag $metadatas, string $destination = null, array $resolutionXY = array()): ?int 235 | { 236 | if ( ! file_exists($file)) { 237 | throw new InvalidArgumentException(sprintf('%s does not exists', $file)); 238 | } 239 | 240 | // if the -o file exists, exiftool prints an error 241 | if($destination) { 242 | @unlink($destination); 243 | if (file_exists($destination)) { 244 | throw new InvalidArgumentException(sprintf('%s cannot be replaced', $destination)); 245 | } 246 | } 247 | 248 | $common_args = [ 249 | '-ignoreMinorErrors', 250 | '-preserve', 251 | '-charset UTF8' 252 | ]; 253 | 254 | $commands_groups = []; 255 | 256 | if ($this->erase) { 257 | /** 258 | * if erase is specfied, we MUST start by erasing datas before doing 259 | * anything else. 260 | */ 261 | $commands = ['-all:all=']; 262 | if(!$this->eraseProfile) { 263 | $commands[] = '--icc_profile:all'; 264 | } 265 | $commands_groups[] = $commands; 266 | } 267 | 268 | if(count($resolutionXY) == 2 && is_int(current($resolutionXY)) && is_int(end($resolutionXY)) ){ 269 | reset($resolutionXY); 270 | $commands_groups[] = [ 271 | '-xresolution=' . current($resolutionXY), 272 | '-yresolution=' . end($resolutionXY) 273 | ]; 274 | } 275 | 276 | if(count($metadatas) > 0) { 277 | $commands_groups[] = $this->addMetadatasArg($metadatas); 278 | $common_args[] = '-codedcharacterset=utf8'; 279 | } 280 | 281 | $commands_groups[] = $this->getSyncCommand(); 282 | 283 | if(count($commands_groups) == 0) { 284 | // nothing to do... 285 | if($destination) { 286 | // ... but a destination 287 | $commands_groups[] = []; // empty command so exiftool will copy the file for us 288 | } 289 | else { 290 | // really nothing to do = 0 ops 291 | return 1; // considered as "unchanged" 292 | } 293 | } 294 | 295 | if($destination) { 296 | foreach($commands_groups as $i=>$commands) { 297 | if($i==0) { 298 | // the FIRST command will -o the destination 299 | $commands_groups[0][] = $file; 300 | $commands_groups[0][] = '-o'; 301 | $commands_groups[0][] = $destination; 302 | } 303 | else { 304 | // then the next commands will work on the destination 305 | $commands_groups[$i][] = '-overwrite_original_in_place'; 306 | $commands_groups[$i][] = $destination; 307 | } 308 | } 309 | } 310 | else { 311 | // every command (even a single one) works on the original file 312 | $common_args[] = '-overwrite_original_in_place '; 313 | $common_args[] = $file; 314 | } 315 | 316 | 317 | if(count($commands_groups) > 1) { 318 | // really need "-common_args" only if many commands are chained 319 | // nb: the file argument CAN be into -common_args 320 | array_unshift($common_args, '-common_args'); 321 | } 322 | 323 | $commands = []; 324 | foreach ($commands_groups as $i => $cg) { 325 | if($i > 0) { 326 | $commands[] = '-execute'; 327 | } 328 | foreach($cg as $c) { 329 | $commands[] = $c; 330 | } 331 | } 332 | foreach ($common_args as $a) { 333 | $commands[] = $a; 334 | } 335 | 336 | $ret = $this->exiftool->executeCommand($commands, $this->timeout); 337 | 338 | // exiftool may print (return) a bunch of lines, even for a single command 339 | // e.g. deleting tags of a file with NO tags may return 2 lines... 340 | // | exiftool -all:all= notags.jpg 341 | // | 0 image files updated 342 | // | 1 image files unchanged 343 | // ... which is NOT an error, 344 | // so it's not easy to decide from the output when something went REALLY wrong 345 | $n_unchanged = $n_changed = 0; 346 | foreach(explode("\n", $ret) as $line) { 347 | if (preg_match("/(\\d+) image files (copied|created|updated|unchanged)/", $line, $matches)) { 348 | if($matches[2] == 'unchanged') { 349 | $n_unchanged += (int)($matches[1]); 350 | } 351 | else { 352 | $n_changed += (int)($matches[1]); 353 | } 354 | } 355 | } 356 | // first chance, changes happened 357 | if($n_changed > 0) { 358 | // return $n_changed; // nice but breaks backward compatibility 359 | return 1; // better, backward compatible and tests are ok 360 | } 361 | // second chance, at least one no-op happened 362 | if($n_unchanged > 0) { 363 | return 1; 364 | } 365 | // too bad 366 | return null; 367 | } 368 | 369 | /** 370 | * Computes modes, modules and metadatas to a single commandline 371 | * 372 | * @param MetadataBag $metadatas A Bag of metadatas 373 | * 374 | * @return array parts of the command 375 | */ 376 | protected function addMetadatasArg(MetadataBag $metadatas): array 377 | { 378 | $commands = []; 379 | 380 | if ($this->modules & self::MODULE_MWG) { 381 | $commands[] = '-use'; 382 | $commands[] = 'MWG'; 383 | } 384 | 385 | /** @var Metadata $metadata */ 386 | foreach ($metadatas as $metadata) { 387 | foreach ($metadata->getValue()->asArray() as $value) { 388 | $commands[] = '-' . $metadata->getTagGroup()->getWriteKey() . '=' . $value; 389 | } 390 | } 391 | 392 | return $commands; 393 | } 394 | 395 | protected function getSyncCommand(): array 396 | { 397 | $syncCommands = []; 398 | 399 | $availableArgs = array( 400 | self::MODE_IPTC2XMP => 'iptc2xmp.args', 401 | self::MODE_IPTC2EXIF => 'iptc2exif.args', 402 | self::MODE_EXIF2IPTC => 'exif2iptc.args', 403 | self::MODE_EXIF2XMP => 'exif2xmp.args', 404 | self::MODE_PDF2XMP => 'pdf2xmp.args', 405 | self::MODE_XMP2PDF => 'xmp2pdf.args', 406 | self::MODE_GPS2XMP => 'gps2xmp.args', 407 | self::MODE_XMP2EXIF => 'xmp2exif.args', 408 | self::MODE_XMP2IPTC => 'xmp2iptc.args', 409 | self::MODE_XMP2GPS => 'xmp2gps.args', 410 | ); 411 | 412 | foreach ($availableArgs as $arg => $cmd) { 413 | if ($this->mode & $arg) { 414 | $syncCommands[] = '-@'; 415 | $syncCommands[] = $cmd; 416 | } 417 | } 418 | 419 | return $syncCommands; 420 | } 421 | } 422 | 423 | -------------------------------------------------------------------------------- /tests/lib/PHPExiftool/WriterTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace lib\PHPExiftool; 12 | 13 | use PHPExiftool\Driver; 14 | use PHPExiftool\Driver\TagGroupInterface; 15 | use PHPExiftool\Exception\InvalidArgumentException; 16 | use PHPExiftool\PHPExiftool; 17 | use PHPExiftool\Writer; 18 | use PHPExiftool\Reader; 19 | use PHPExiftool\Exception\EmptyCollectionException; 20 | use PHPUnit\Framework\TestCase; 21 | 22 | class WriterTester extends Writer 23 | { 24 | private Writer $writer; 25 | 26 | public function __construct(Writer $writer) 27 | { 28 | parent::__construct($writer->exiftool); 29 | $this->writer = $writer; 30 | } 31 | 32 | public function __call($name, $args) 33 | { 34 | return call_user_func($this->writer->{$name}, $args); 35 | } 36 | 37 | public function addMetadatasArgTester($metadatas): array 38 | { 39 | return $this->writer->addMetadatasArg($metadatas); 40 | } 41 | 42 | public function getSyncCommandTester(): array 43 | { 44 | return $this->writer->getSyncCommand(); 45 | } 46 | } 47 | 48 | class WriterTest extends TestCase 49 | { 50 | protected ?PHPExiftool $PHPExiftool = null; 51 | protected string $in = ""; 52 | protected string $inWithICC = ""; 53 | protected string $inPlace = ""; 54 | protected string $out = ""; 55 | 56 | protected function setUp(): void 57 | { 58 | parent::setUp(); 59 | $this->in = __DIR__ . '/../../files/ExifTool.jpg'; 60 | $this->inWithICC = __DIR__ . '/../../files/pixelWithIcc.jpg'; 61 | $this->out = __DIR__ . '/../../files/ExifTool_erased.jpg'; 62 | $this->inPlace = __DIR__ . '/../../files/ExifToolCopied.jpg'; 63 | copy($this->in, $this->inPlace); 64 | 65 | $this->PHPExiftool = new PHPExiftool("/tmp"); 66 | } 67 | 68 | protected function tearDown(): void 69 | { 70 | if (file_exists($this->out) && is_writable($this->out)) { 71 | unlink($this->out); 72 | } 73 | if (file_exists($this->inPlace) && is_writable($this->inPlace)) { 74 | unlink($this->inPlace); 75 | } 76 | } 77 | 78 | private function createWriter(): Writer 79 | { 80 | return $this->PHPExiftool->getFactory()->createWriter(); 81 | } 82 | 83 | private function createReader(): Reader 84 | { 85 | return $this->PHPExiftool->getFactory()->createReader(); 86 | } 87 | 88 | private function createTagGroup(string $tagName): TagGroupInterface 89 | { 90 | return $this->PHPExiftool->getFactory()->createTagGroup($tagName); 91 | } 92 | 93 | 94 | 95 | /** 96 | * @covers Writer::setMode 97 | * @covers Writer::isMode 98 | */ 99 | public function testSetMode() 100 | { 101 | $writer = $this->createWriter(); 102 | 103 | $writer->setMode(Writer::MODE_EXIF2IPTC, true); 104 | $this->assertTrue($writer->isMode(Writer::MODE_EXIF2IPTC)); 105 | $writer->setMode(Writer::MODE_XMP2EXIF, true); 106 | $this->assertTrue($writer->isMode(Writer::MODE_XMP2EXIF)); 107 | $writer->setMode(Writer::MODE_EXIF2IPTC, false); 108 | $this->assertFalse($writer->isMode(Writer::MODE_EXIF2IPTC)); 109 | $writer->setMode(Writer::MODE_XMP2EXIF, true); 110 | $this->assertTrue($writer->isMode(Writer::MODE_XMP2EXIF)); 111 | } 112 | 113 | /** 114 | * @covers Writer::copy 115 | * @throws EmptyCollectionException 116 | */ 117 | public function testCopy() 118 | { 119 | $writer = $this->createWriter(); 120 | 121 | $metadatas = new Driver\Metadata\MetadataBag(); 122 | $writer->erase(true, true); 123 | $changedFiles = $writer->write($this->inWithICC, $metadatas, $this->out); 124 | $this->assertEquals(1, $changedFiles); 125 | 126 | $reader = $this->createReader(); 127 | $metadatasRead = $reader->files($this->out)->first()->getMetadatas(); 128 | $this->assertFalse(is_object($metadatasRead->get('IPTC:ObjectName'))); 129 | 130 | $writer->copy($this->in, $this->out); 131 | 132 | $metadatasRead = $reader->files($this->out)->first()->getMetadatas(); 133 | $this->assertTrue(is_object($metadatasRead->get('IPTC:ObjectName'))); 134 | $this->assertEquals("Test IPTC picture", $metadatasRead->get('IPTC:ObjectName')->getValue()->asString()); 135 | } 136 | 137 | /** 138 | * @covers Writer::setModule 139 | * @covers Writer::hasModule 140 | */ 141 | public function testSetModule() 142 | { 143 | $writer = $this->createWriter(); 144 | 145 | $this->assertFalse($writer->hasModule(Writer::MODULE_MWG)); 146 | $writer->setModule(Writer::MODULE_MWG, true); 147 | $this->assertTrue($writer->hasModule(Writer::MODULE_MWG)); 148 | $writer->setModule(Writer::MODULE_MWG, false); 149 | $this->assertFalse($writer->hasModule(Writer::MODULE_MWG)); 150 | } 151 | 152 | /** 153 | * @covers Writer::write 154 | * @covers Writer::erase 155 | * @throws EmptyCollectionException 156 | */ 157 | public function testEraseWithoutICC() 158 | { 159 | $writer = $this->createWriter(); 160 | $reader = $this->createReader(); 161 | 162 | $uniqueId = 'UNI-QUE-ID'; 163 | 164 | $metadatas = new Driver\Metadata\MetadataBag(); 165 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("IPTC:UniqueDocumentID"), new Driver\Value\Mono($uniqueId))); 166 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("XMP_exif:ImageUniqueID"), new Driver\Value\Mono($uniqueId))); 167 | 168 | $writer->erase(true, false); 169 | $changedFiles = $writer->write($this->inWithICC, $metadatas, $this->out); 170 | $this->assertEquals(1, $changedFiles); 171 | 172 | $this->assertGreaterThan(200, count($reader->files($this->in)->first()->getMetadatas())); 173 | 174 | $reader->reset(); 175 | $this->assertGreaterThan(4, count($reader->files($this->out)->first()->getMetadatas())); 176 | $this->assertLessThan(30, count($reader->files($this->out)->first()->getMetadatas())); 177 | 178 | $acceptedMetas = [ 179 | 'Exiftool:\w+', 180 | 'System:\w+', 181 | 'File:\w+', 182 | 'Composite:\w+', 183 | 'IPTC:CodedCharacterSet', 184 | 'IPTC:EnvelopeRecordVersion', 185 | 'IPTC:UniqueDocumentID', 186 | 'IPTC:ApplicationRecordVersion', 187 | 'Photoshop:IPTCDigest', 188 | 'XMP-x:XMPToolkit', 189 | 'XMP-exif:ImageUniqueID', 190 | 'Adobe:DCTEncodeVersion', 191 | 'Adobe:APP14Flags0', 192 | 'Adobe:APP14Flags1', 193 | 'Adobe:ColorTransform', 194 | ]; 195 | 196 | foreach ($reader->files($this->out)->first()->getMetadatas() as $meta) { 197 | 198 | $found = false; 199 | 200 | foreach ($acceptedMetas as $accepted) { 201 | if (preg_match('/' . $accepted . '/i', $meta->getTagGroup()->getId())) { 202 | $found = true; 203 | break; 204 | } 205 | } 206 | 207 | if (!$found) { 208 | $this->fail(sprintf('Unexpected meta %s found', $meta->getTagGroup()->getId())); 209 | } 210 | } 211 | } 212 | 213 | /** 214 | * @throws EmptyCollectionException 215 | */ 216 | public function testEraseWithICC() 217 | { 218 | $writer = $this->createWriter(); 219 | $reader = $this->createReader(); 220 | 221 | $uniqueId = 'UNI-QUE-ID'; 222 | 223 | $metadatas = new Driver\Metadata\MetadataBag(); 224 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("IPTC:UniqueDocumentID"), new Driver\Value\Mono($uniqueId))); 225 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("XMP_exif:ImageUniqueID"), new Driver\Value\Mono($uniqueId))); 226 | 227 | $writer->erase(true, true); 228 | $changedFiles = $writer->write($this->inWithICC, $metadatas, $this->out); 229 | $this->assertEquals(1, $changedFiles); 230 | 231 | $this->assertGreaterThan(200, count($reader->files($this->in)->first()->getMetadatas())); 232 | 233 | $reader->reset(); 234 | $this->assertGreaterThan(4, count($reader->files($this->out)->first()->getMetadatas())); 235 | 236 | $acceptedMetas = [ 237 | 'Exiftool:\w+', 238 | 'System:\w+', 239 | 'File:\w+', 240 | 'Composite:\w+', 241 | 'IPTC:CodedCharacterSet', 242 | 'ICC-header:\w+', 243 | 'IPTC:EnvelopeRecordVersion', 244 | 'IPTC:UniqueDocumentID', 245 | 'IPTC:ApplicationRecordVersion', 246 | 'Photoshop:IPTCDigest', 247 | 'XMP-x:XMPToolkit', 248 | 'XMP-exif:ImageUniqueID', 249 | 'Adobe:DCTEncodeVersion', 250 | 'Adobe:APP14Flags0', 251 | 'Adobe:APP14Flags1', 252 | 'Adobe:ColorTransform', 253 | ]; 254 | 255 | foreach ($reader->files($this->out)->first()->getMetadatas() as $meta) { 256 | 257 | $found = false; 258 | 259 | foreach ($acceptedMetas as $accepted) { 260 | if (preg_match('/' . $accepted . '/i', $meta->getTagGroup()->getId())) { 261 | $found = true; 262 | break; 263 | } 264 | } 265 | 266 | if (!$found) { 267 | $this->fail(sprintf('Unexpected meta %s found', $meta->getTagGroup()->getId())); 268 | } 269 | } 270 | } 271 | 272 | /** 273 | * @covers Writer::write 274 | * @throws EmptyCollectionException 275 | */ 276 | public function testWrite() 277 | { 278 | $writer = $this->createWriter(); 279 | $reader = $this->createReader(); 280 | 281 | $metadatas = new Driver\Metadata\MetadataBag(); 282 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("IPTC:ObjectName"), new Driver\Value\Mono('Beautiful Object'))); 283 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("IPTC:ObjectName"), new Driver\Value\Mono('Beautiful Object'))); 284 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("XMP_iptcExt:PersonInImage"), new Driver\Value\Multi(['Romain', 'Nicolas']))); 285 | 286 | $changedFiles = $writer->write($this->in, $metadatas, $this->out); 287 | 288 | $this->assertEquals(1, $changedFiles); 289 | 290 | $metadatasRead = $reader->files($this->out)->first()->getMetadatas(); 291 | 292 | $this->assertGreaterThan(200, count($metadatasRead)); 293 | 294 | $this->assertEquals('Beautiful Object', $metadatasRead->get('IPTC:ObjectName')->getValue()->asString()); 295 | $this->assertEquals(['Romain', 'Nicolas'], $metadatasRead->get('XMP-iptcExt:PersonInImage')->getValue()->asArray()); 296 | } 297 | 298 | /** 299 | * @covers Writer::write 300 | * @throws EmptyCollectionException 301 | */ 302 | public function testWriteInPlace() 303 | { 304 | $writer = $this->createWriter(); 305 | $reader = $this->createReader(); 306 | 307 | $metadatas = new Driver\Metadata\MetadataBag(); 308 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("IPTC:ObjectName"), new Driver\Value\Mono('Beautiful Object'))); 309 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("IPTC:ObjectName"), new Driver\Value\Mono('Beautiful Object'))); 310 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("XMP_iptcExt:PersonInImage"), new Driver\Value\Multi(['Romain', 'Nicolas']))); 311 | 312 | $changedFiles = $writer->write($this->inPlace, $metadatas); 313 | 314 | $this->assertEquals(1, $changedFiles); 315 | 316 | $metadatasRead = $reader->files($this->inPlace)->first()->getMetadatas(); 317 | 318 | $this->assertGreaterThan(200, count($metadatasRead)); 319 | 320 | $this->assertEquals('Beautiful Object', $metadatasRead->get('IPTC:ObjectName')->getValue()->asString()); 321 | $this->assertEquals(['Romain', 'Nicolas'], $metadatasRead->get('XMP-iptcExt:PersonInImage')->getValue()->asArray()); 322 | } 323 | 324 | /** 325 | * @covers Writer::write 326 | * @throws EmptyCollectionException 327 | */ 328 | public function testWriteInPlaceErased() 329 | { 330 | $writer = $this->createWriter(); 331 | $reader = $this->createReader(); 332 | 333 | $metadatas = new Driver\Metadata\MetadataBag(); 334 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("IPTC:ObjectName"), new Driver\Value\Mono('Beautiful Object'))); 335 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("IPTC:ObjectName"), new Driver\Value\Mono('Beautiful Object'))); 336 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("XMP_iptcExt:PersonInImage"), new Driver\Value\Multi(['Romain', 'Nicolas']))); 337 | 338 | $writer->erase(true); 339 | $changedFiles = $writer->write($this->inPlace, $metadatas); 340 | 341 | $this->assertEquals(1, $changedFiles); 342 | 343 | $metadatasRead = $reader->files($this->inPlace)->first()->getMetadatas(); 344 | 345 | $this->assertLessThan(50, count($metadatasRead)); 346 | 347 | $this->assertEquals('Beautiful Object', $metadatasRead->get('IPTC:ObjectName')->getValue()->asString()); 348 | $this->assertEquals(['Romain', 'Nicolas'], $metadatasRead->get('XMP-iptcExt:PersonInImage')->getValue()->asArray()); 349 | } 350 | 351 | /** 352 | * @covers Writer::write 353 | * @covers InvalidArgumentException 354 | */ 355 | public function testWriteFail() 356 | { 357 | $writer = $this->createWriter(); 358 | 359 | $this->expectException(InvalidArgumentException::class); 360 | $writer->write('ici', new Driver\Metadata\MetadataBag()); 361 | } 362 | 363 | /** 364 | * @covers Writer::addMetadatasArg 365 | */ 366 | public function testAddMetadatasArg() 367 | { 368 | $this->markTestIncomplete(); 369 | 370 | /* todo : fix decorator 371 | $writer = new WriterTester($this->createWriter()); 372 | 373 | $modes = [ 374 | Writer::MODE_EXIF2IPTC => ['-@', 'exif2iptc.args'], 375 | Writer::MODE_EXIF2XMP => ['-@', 'exif2xmp.args'], 376 | Writer::MODE_IPTC2EXIF => ['-@', 'iptc2exif.args'], 377 | Writer::MODE_IPTC2XMP => ['-@', 'iptc2xmp.args'], 378 | Writer::MODE_GPS2XMP => ['-@', 'gps2xmp.args'], 379 | Writer::MODE_PDF2XMP => ['-@', 'pdf2xmp.args'], 380 | Writer::MODE_XMP2PDF => ['-@', 'xmp2pdf.args'], 381 | Writer::MODE_XMP2GPS => ['-@', 'xmp2gps.args'], 382 | Writer::MODE_XMP2EXIF => ['-@', 'xmp2exif.args'], 383 | Writer::MODE_XMP2IPTC => ['-@', 'xmp2iptc.args'], 384 | ]; 385 | 386 | $metadatas = new Driver\Metadata\MetadataBag(); 387 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("IPTC:ObjectName"), new Driver\Value\Mono('Beautiful Object'))); 388 | $metadatas->add(new Driver\Metadata\Metadata($this->createTagGroup("XMP_iptcExt:PersonInImage"), new Driver\Value\Multi(['Romain', 'Nicolas']))); 389 | 390 | $this->assertEmpty($writer->getSyncCommandTester()); 391 | 392 | // modes accumulate 393 | $a = []; 394 | foreach ($modes as $k => $v) { 395 | $writer->setMode($k, true); 396 | $a[] = $v; 397 | $this->_testContains($writer->getSyncCommandTester(), $a); 398 | } 399 | 400 | // unset a mode 401 | $writer->setMode(Writer::MODE_XMP2IPTC, false); 402 | $this->assertNotContains('xmp2iptc', $writer->getSyncCommandTester()); 403 | 404 | 405 | $writer->setModule(Writer::MODULE_MWG, true); 406 | $a = $writer->addMetadatasArgTester($metadatas); 407 | $this->_testContains($a, [['-use', 'MWG']]); 408 | 409 | $writer->setModule(Writer::MODULE_MWG, false); 410 | $this->assertNotContains('MWG', $writer->addMetadatasArgTester($metadatas)); 411 | 412 | $this->assertContains("-XMP-iptcExt:PersonInImage=Nicolas", $writer->addMetadatasArgTester($metadatas)); 413 | */ 414 | } 415 | 416 | private function _testContains($a, $modes) 417 | { 418 | foreach ($modes as $mode) { 419 | $this->assertContains($mode[1], $a); 420 | $p = array_search($mode[1], $a, true); 421 | $this->assertTrue(is_int($p) && $p > 0 && $a[$p - 1] === $mode[0], sprintf("missing \"%s\" before \"%s\"", $mode[0], $mode[1])); 422 | } 423 | } 424 | } 425 | 426 | 427 | -------------------------------------------------------------------------------- /lib/PHPExiftool/Reader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool; 13 | 14 | use Doctrine\Common\Collections\ArrayCollection; 15 | use Exception; 16 | use Iterator; 17 | use IteratorAggregate; 18 | use PHPExiftool\Exception\EmptyCollectionException; 19 | use PHPExiftool\Exception\LogicException; 20 | use PHPExiftool\Exception\RuntimeException; 21 | use Psr\Log\LoggerInterface; 22 | 23 | /** 24 | * 25 | * Exiftool Reader, inspired by Symfony2 Finder. 26 | * 27 | * It scans files and directories, and provide an iterator on the FileEntities 28 | * generated based on the results. 29 | * 30 | * Example usage: 31 | * 32 | * $Reader = new Reader(); 33 | * 34 | * $Reader->in('/path/to/directory') 35 | * ->exclude('tests') 36 | * ->extensions(array('jpg', 'xml)); 37 | * 38 | * //Throws an exception if no file found 39 | * $first = $Reader->first(); 40 | * 41 | * //Returns null if no file found 42 | * $first = $Reader->getOneOrNull(); 43 | * 44 | * foreach($Reader as $entity) 45 | * { 46 | * //Do your logic with FileEntity 47 | * } 48 | * 49 | * 50 | * @todo implement match conditions (-if EXPR) (name or metadata tag) 51 | * @todo implement match filter 52 | * @todo implement sort 53 | * @todo implement -l 54 | * 55 | * @author Romain Neutron 56 | */ 57 | class Reader implements IteratorAggregate 58 | { 59 | protected array $files = []; 60 | protected array $dirs = []; 61 | protected array $excludeDirs = []; 62 | protected array $extensions = []; 63 | protected ?bool $extensionsToggle = null; 64 | protected bool $followSymLinks = false; 65 | protected bool $recursive = true; 66 | protected bool $ignoreDotFile = false; 67 | protected array $sort = []; 68 | protected ?RDFParser $parser; 69 | protected Exiftool $exiftool; 70 | protected int $timeout = 60; 71 | 72 | protected ?ArrayCollection $collection = null; 73 | protected array $readers = []; 74 | 75 | /** 76 | * Constructor 77 | */ 78 | private function __construct(Exiftool $exiftool, RDFParser $parser) 79 | { 80 | $this->exiftool = $exiftool; 81 | $this->parser = $parser; 82 | } 83 | 84 | public static function create(Exiftool $exiftool, RDFParser $parser) 85 | { 86 | return new self($exiftool, $parser); 87 | } 88 | 89 | public function __destruct() 90 | { 91 | $this->parser = null; 92 | $this->collection = null; 93 | } 94 | 95 | public function setTimeout($timeout):self 96 | { 97 | $this->timeout = $timeout; 98 | 99 | return $this; 100 | } 101 | 102 | public function reset(): self 103 | { 104 | $this->files 105 | = $this->dirs 106 | = $this->excludeDirs 107 | = $this->extensions 108 | = $this->sort 109 | = $this->readers = []; 110 | 111 | $this->recursive = true; 112 | $this->ignoreDotFile = $this->followSymLinks = false; 113 | $this->extensionsToggle = null; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Implements \IteratorAggregate Interface 120 | * 121 | * @return Iterator 122 | * @throws Exception 123 | */ 124 | public function getIterator(): Iterator 125 | { 126 | return $this->all()->getIterator(); 127 | } 128 | 129 | /** 130 | * Add files to scan 131 | * 132 | * Example usage: 133 | * 134 | * // Will scan 3 files : dc00.jpg in CWD and absolute 135 | * // paths /tmp/image.jpg and /tmp/raw.CR2 136 | * $Reader ->files('dc00.jpg') 137 | * ->files(array('/tmp/image.jpg', '/tmp/raw.CR2')) 138 | * 139 | * @param string|array $files The files 140 | * @return Reader 141 | */ 142 | public function files($files): self 143 | { 144 | $this->resetResults(); 145 | $this->files = array_merge($this->files, (array)$files); 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * Add dirs to scan 152 | * 153 | * Example usage: 154 | * 155 | * // Will scan 3 dirs : documents in CWD and absolute 156 | * // paths /usr and /var 157 | * $Reader ->in('documents') 158 | * ->in(array('/tmp', '/var')) 159 | * 160 | * @param string|array $dirs The directories 161 | * @return Reader 162 | */ 163 | public function in($dirs): self 164 | { 165 | $this->resetResults(); 166 | $this->dirs = array_merge($this->dirs, (array)$dirs); 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * Append a reader to this one. 173 | * Finale result will be the sum of the current reader and all appended ones. 174 | * 175 | * @param Reader $reader The reader to append 176 | * @return Reader 177 | */ 178 | public function append(Reader $reader): self 179 | { 180 | $this->resetResults(); 181 | $this->readers[] = $reader; 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * Sort results with one or many criteria 188 | * 189 | * Example usage: 190 | * 191 | * // Will sort by directory then filename 192 | * $Reader ->in('documents') 193 | * ->sort(array('directory', 'filename')) 194 | * 195 | * // Will sort by filename 196 | * $Reader ->in('documents') 197 | * ->sort('filename') 198 | * 199 | * @param string|array $by 200 | * @return Reader 201 | */ 202 | public function sort($by): self 203 | { 204 | static $availableSorts = [ 205 | 'directory', 'filename', 'createdate', 'modifydate', 'filesize' 206 | ]; 207 | 208 | foreach ((array)$by as $sort) { 209 | 210 | if (!in_array($sort, $availableSorts)) { 211 | continue; 212 | } 213 | $this->sort[] = $sort; 214 | } 215 | 216 | return $this; 217 | } 218 | 219 | /** 220 | * Exclude directories from scan 221 | * 222 | * Warning: only first depth directories can be excluded 223 | * Imagine a directory structure like below, With a scan in "root", only 224 | * sub1 or sub2 can be excluded, not subsub. 225 | * 226 | * root 227 | * ├── sub1 228 | * └── sub2 229 | *    └── subsub 230 | * 231 | * Example usage: 232 | * 233 | * // Will scan documents recursively, discarding documents/test 234 | * $Reader ->in('documents') 235 | * ->exclude(array('test')) 236 | * 237 | * @param string|array $dirs The directories 238 | * @return Reader 239 | */ 240 | public function exclude($dirs): self 241 | { 242 | $this->resetResults(); 243 | $this->excludeDirs = array_merge($this->excludeDirs, (array)$dirs); 244 | 245 | return $this; 246 | } 247 | 248 | /** 249 | * Restrict / Discard files based on extensions. 250 | * Extensions are case_insensitive. 251 | * 252 | * @param string|array $extensions The list of extension 253 | * @param Boolean $restrict Toggle restrict/discard method 254 | * @return Reader 255 | * @throws LogicException 256 | */ 257 | public function extensions($extensions, bool $restrict = true): self 258 | { 259 | $this->resetResults(); 260 | 261 | if (!is_null($this->extensionsToggle)) { 262 | if ($restrict !== $this->extensionsToggle) { 263 | throw new LogicException('You cannot restrict extensions AND exclude extension at the same time'); 264 | } 265 | } 266 | 267 | $this->extensionsToggle = (boolean)$restrict; 268 | 269 | $this->extensions = array_merge($this->extensions, (array)$extensions); 270 | 271 | return $this; 272 | } 273 | 274 | /** 275 | * Toggle to enable follow Symbolic Links 276 | * 277 | * @return Reader 278 | */ 279 | public function followSymLinks(): self 280 | { 281 | $this->resetResults(); 282 | $this->followSymLinks = true; 283 | 284 | return $this; 285 | } 286 | 287 | /** 288 | * Ignore files starting with a dot (.) 289 | * 290 | * Folders starting with a dot are always exluded due to exiftool behaviour. 291 | * You should include them manually 292 | * 293 | * @return Reader 294 | */ 295 | public function ignoreDotFiles(): self 296 | { 297 | $this->resetResults(); 298 | $this->ignoreDotFile = true; 299 | 300 | return $this; 301 | } 302 | 303 | /** 304 | * Disable recursivity in directories scan. 305 | * If you only specify files, this toggle has no effect 306 | * 307 | * @return Reader 308 | */ 309 | public function notRecursive(): self 310 | { 311 | $this->resetResults(); 312 | $this->recursive = false; 313 | 314 | return $this; 315 | } 316 | 317 | /** 318 | * Return the first result. If no result available, null is returned 319 | * 320 | * @return FileEntity 321 | * @throws Exception 322 | */ 323 | public function getOneOrNull(): ?FileEntity 324 | { 325 | return count($this->all()) === 0 ? null : $this->all()->first(); 326 | } 327 | 328 | /** 329 | * Return the first result. If no result available, throws an exception 330 | * 331 | * @return FileEntity 332 | * @throws EmptyCollectionException 333 | * @throws Exception 334 | */ 335 | public function first(): FileEntity 336 | { 337 | if (count($this->all()) === 0) { 338 | throw new EmptyCollectionException('Collection is empty'); 339 | } 340 | 341 | return $this->all()->first(); 342 | } 343 | 344 | /** 345 | * Perform the scan and returns all the results 346 | * 347 | * @return ArrayCollection 348 | * @throws Exception 349 | */ 350 | public function all(): ?ArrayCollection 351 | { 352 | if (!$this->collection) { 353 | $this->collection = $this->buildQueryAndExecute(); 354 | } 355 | 356 | if ($this->readers) { 357 | $elements = $this->collection->toArray(); 358 | 359 | $this->collection = null; 360 | 361 | foreach ($this->readers as $reader) { 362 | $elements = array_merge($elements, $reader->all()->toArray()); 363 | } 364 | 365 | $this->collection = new ArrayCollection($elements); 366 | } 367 | 368 | return $this->collection; 369 | } 370 | 371 | /** 372 | * Reset any computed result 373 | * 374 | * @return Reader 375 | */ 376 | protected function resetResults(): self 377 | { 378 | $this->collection = null; 379 | 380 | return $this; 381 | } 382 | 383 | /** 384 | * Build the command returns an ArrayCollection of FileEntity 385 | * 386 | * @return ArrayCollection 387 | * @throws Exception 388 | */ 389 | protected function buildQueryAndExecute(): ArrayCollection 390 | { 391 | $result = ''; 392 | 393 | try { 394 | $result = trim($this->exiftool->executeCommand($this->buildQuery(), $this->timeout)); 395 | } 396 | catch (RuntimeException $e) { 397 | /** 398 | * In case no file found, an exit code 1 is returned 399 | */ 400 | if (!$this->ignoreDotFile) { 401 | throw $e; 402 | } 403 | } 404 | 405 | if ($result === '') { 406 | return new ArrayCollection(); 407 | } 408 | 409 | $this->parser->open($result); 410 | 411 | return $this->parser->ParseEntities(); 412 | } 413 | 414 | /** 415 | * Compute raw exclude rules to simple ones, based on exclude dirs and search dirs 416 | * 417 | * @param string[] $rawExcludeDirs 418 | * @param string[] $rawSearchDirs 419 | * @return array 420 | * @throws RuntimeException 421 | */ 422 | protected function computeExcludeDirs(array $rawExcludeDirs, array $rawSearchDirs): array 423 | { 424 | $excludeDirs = []; 425 | 426 | foreach ($rawExcludeDirs as $excludeDir) { 427 | $found = false; 428 | /** 429 | * is this a relative path ? 430 | */ 431 | foreach ($rawSearchDirs as $dir) { 432 | $currentPrefix = realpath($dir) . DIRECTORY_SEPARATOR; 433 | 434 | $supposedExcluded = str_replace($currentPrefix, '', realpath($currentPrefix . $excludeDir)); 435 | 436 | if (!$supposedExcluded) { 437 | continue; 438 | } 439 | 440 | if (strpos($supposedExcluded, DIRECTORY_SEPARATOR) === false) { 441 | $excludeDirs[] = $supposedExcluded; 442 | $found = true; 443 | break; 444 | } 445 | } 446 | 447 | if ($found) { 448 | continue; 449 | } 450 | 451 | /** 452 | * is this an absolute path ? 453 | */ 454 | $supposedExcluded = realpath($excludeDir); 455 | 456 | if ($supposedExcluded) { 457 | foreach ($rawSearchDirs as $dir) { 458 | $searchDir = realpath($dir) . DIRECTORY_SEPARATOR; 459 | 460 | $supposedRelative = str_replace($searchDir, '', $supposedExcluded); 461 | 462 | if (strpos($supposedRelative, DIRECTORY_SEPARATOR) !== false) { 463 | continue; 464 | } 465 | 466 | if (strpos($supposedExcluded, $searchDir) !== 0) { 467 | continue; 468 | } 469 | 470 | if (!trim($supposedRelative)) { 471 | continue; 472 | } 473 | 474 | $excludeDirs[] = $supposedRelative; 475 | $found = true; 476 | break; 477 | } 478 | } 479 | 480 | 481 | if (!$found) { 482 | throw new RuntimeException(sprintf("Invalid exclude dir %s ; Exclude dir is limited to the name of a directory at first depth", $excludeDir)); 483 | } 484 | } 485 | 486 | return $excludeDirs; 487 | } 488 | 489 | /** 490 | * Build query from criterias 491 | * 492 | * @return string[] 493 | * 494 | * @throws LogicException 495 | */ 496 | protected function buildQuery(): array 497 | { 498 | if (!$this->dirs && !$this->files) { 499 | throw new LogicException('You have not set any files or directory'); 500 | } 501 | 502 | $command = [ 503 | '-n', // disable print conversion 504 | '-q', // quiet 505 | '-b', // some binary 506 | '-X', // XML 507 | '-charset', 'UTF8' 508 | ]; 509 | // $command = [ 510 | // '-n', 511 | // '-q', 512 | // '-b', 513 | // '-j', // json 514 | // '-D', // decimal tag id 515 | // '-t', // add table info 516 | // '-charset', 'UTF8' 517 | // ]; 518 | 519 | if ($this->recursive) { 520 | $command[] = '-r'; 521 | } 522 | 523 | if (!empty($this->extensions)) { 524 | if (!$this->extensionsToggle) { 525 | $extensionPrefix = '--ext'; 526 | } 527 | else { 528 | $extensionPrefix = '-ext'; 529 | } 530 | 531 | foreach ($this->extensions as $extension) { 532 | $command[] = $extensionPrefix; 533 | $command[] = $extension; 534 | } 535 | } 536 | 537 | if (!$this->followSymLinks) { 538 | $command[] = '-i'; 539 | $command[] = 'SYMLINKS'; 540 | } 541 | 542 | if ($this->ignoreDotFile) { 543 | $command[] = '-if'; 544 | $command[] = "'\$filename !~ /^\./'"; 545 | } 546 | 547 | foreach ($this->sort as $sort) { 548 | $command[] = '-fileOrder'; 549 | $command[] = $sort; 550 | } 551 | 552 | foreach ($this->computeExcludeDirs($this->excludeDirs, $this->dirs) as $excludedDir) { 553 | $command[] = '-i'; 554 | $command[] = $excludedDir; 555 | } 556 | 557 | foreach ($this->dirs as $dir) { 558 | $command[] = realpath($dir); 559 | } 560 | 561 | foreach ($this->files as $file) { 562 | $command[] = realpath($file); 563 | } 564 | 565 | return $command; 566 | } 567 | } 568 | -------------------------------------------------------------------------------- /lib/PHPExiftool/InformationDumper.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace PHPExiftool; 13 | 14 | use DOMDocument; 15 | use DOMElement; 16 | use Exception; 17 | use PHPExiftool\ClassUtils\tagGroupBuilder; 18 | use PHPExiftool\Exception\InvalidArgumentException; 19 | use Psr\Log\LoggerInterface; 20 | use Psr\Log\NullLogger; 21 | use Symfony\Component\Console\Helper\ProgressBar; 22 | use Symfony\Component\Console\Input\InputInterface; 23 | use Symfony\Component\Console\Output\OutputInterface; 24 | use Symfony\Component\DomCrawler\Crawler; 25 | use Symfony\Component\Finder\Exception\DirectoryNotFoundException; 26 | 27 | class InformationDumper 28 | { 29 | /** 30 | * For use with list option 31 | */ 32 | const LISTTYPE_WRITABLE = 'w'; 33 | /** 34 | * For use with list option 35 | */ 36 | const LISTTYPE_SUPPORTED_FILEEXT = 'f'; 37 | /** 38 | * For use with list option 39 | */ 40 | const LISTTYPE_WRITABLE_FILEEXT = 'wf'; 41 | /** 42 | * For use with list option 43 | */ 44 | const LISTTYPE_SUPPORTED_XML = 'x'; 45 | /** 46 | * For use with list option 47 | */ 48 | const LISTTYPE_DELETABLE_GROUPS = 'd'; 49 | /** 50 | * For use with list option 51 | */ 52 | const LISTTYPE_GROUPS = 'g'; 53 | 54 | const LISTOPTION_MWG = '-use MWG'; 55 | 56 | private Exiftool $exiftool; 57 | private LoggerInterface $logger; 58 | private int $currentXmlLine; 59 | private string $rootNamespace; 60 | 61 | 62 | public function __construct(Exiftool $exiftool) 63 | { 64 | $this->exiftool = $exiftool; 65 | $this->logger = new NullLogger(); 66 | $this->rootNamespace = PHPExiftool::ROOT_NAMESPACE . '\\' . PHPExiftool::SUBDIR; 67 | } 68 | 69 | public function setLogger(LoggerInterface $logger) 70 | { 71 | $this->logger = $logger; 72 | 73 | return $this; 74 | } 75 | 76 | 77 | /** 78 | * Return the result of an Exiftool -list* command 79 | * 80 | * @see http://www.sno.phy.queensu.ca/~phil/exiftool/exiftool_pod.html#item__2dlist_2c__2dlistw_2c__2dlistf_2c__2dlistr_2c__2d 81 | * @param string $type One of the LISTTYPE_* constants 82 | * @param array $options 83 | * @return DOMDocument 84 | * @throws Exception 85 | */ 86 | public function listDatas(string $type = self::LISTTYPE_SUPPORTED_XML, array $options = [], array $lngs): DOMDocument 87 | { 88 | if (!is_array($options)) { 89 | throw new InvalidArgumentException('options must be an array'); 90 | } 91 | 92 | $available = [ 93 | self::LISTTYPE_WRITABLE, self::LISTTYPE_SUPPORTED_FILEEXT, 94 | self::LISTTYPE_WRITABLE_FILEEXT, self::LISTTYPE_SUPPORTED_XML, 95 | self::LISTTYPE_DELETABLE_GROUPS, self::LISTTYPE_GROUPS, 96 | ]; 97 | 98 | if (!in_array($type, $available)) { 99 | throw new InvalidArgumentException('Unknown list attribute'); 100 | } 101 | 102 | $command = []; 103 | $available = [self::LISTOPTION_MWG]; 104 | foreach ($options as $option) { 105 | if (!in_array($option, $available)) { 106 | throw new InvalidArgumentException('Unknown option'); 107 | } 108 | $command = array_merge($command, explode(' ', $option)); 109 | } 110 | $command[] = '-f'; 111 | // $command[] = '-s'; 112 | foreach ($lngs as $lng) { 113 | $command[] = '-lang'; 114 | $command[] = $lng; 115 | } 116 | $command[] = '-list' . $type; 117 | 118 | $xml = $this->exiftool->executeCommand($command); 119 | $dom = new DOMDocument(); 120 | $dom->loadXML($xml, 4194304 /* XML_PARSE_BIG_LINES */); 121 | 122 | return $dom; 123 | } 124 | 125 | public function dumpClasses(array $options, array $lngs, callable $callback = null) 126 | { 127 | $dom = $this->listDatas(InformationDumper::LISTTYPE_SUPPORTED_XML, $options, $lngs); 128 | 129 | $nTags = 0; 130 | 131 | /** @var tagGroupBuilder[] $tagGroupBuilders */ 132 | $tagGroupBuilders = []; 133 | $group_ids = []; // to check group_id belongs to only one class 134 | 135 | $crawler = new Crawler(); 136 | $crawler->addDocument($dom); 137 | 138 | $tag_count = count($crawler->filter('table>tag')); 139 | $this->logger->info(sprintf('tag count : %d', $tag_count)); 140 | 141 | foreach ($crawler->filter('table') as $table) { 142 | $table_crawler = new Crawler(); 143 | $table_crawler->addNode($table); 144 | 145 | $table_g0 = $table_crawler->attr('g0'); 146 | $table_g1 = $table_crawler->attr('g1'); 147 | $table_g2 = $table_crawler->attr('g2'); 148 | $table_name = $table_crawler->attr('name'); 149 | 150 | $tags = $table_crawler->filter('tag'); 151 | 152 | /** @var DOMElement $tag */ 153 | foreach ($tags as $tag) { 154 | 155 | $this->currentXmlLine = $tag->getLineNo(); 156 | 157 | $tag_crawler = new Crawler(); 158 | $tag_crawler->addNode($tag); 159 | 160 | $tag_name = $tag_crawler->attr('name'); 161 | if (strtoupper($tag_name) === "RESERVED") { 162 | continue; 163 | } 164 | 165 | $tag_g0 = $tag_crawler->attr('g0'); 166 | $tag_g1 = $tag_crawler->attr('g1'); 167 | $tag_g2 = $tag_crawler->attr('g2'); 168 | if ($tag_g0 === '*' || $tag_g1 === '*' || $tag_g2 === '*') { 169 | continue; 170 | } 171 | 172 | $tag_id = $tag_crawler->attr('id'); 173 | if (is_null($tag_id)) { 174 | $this->logger->alert(sprintf("TagGroup has no id.")); 175 | continue; 176 | } 177 | $tag_type = $tag_crawler->attr('type'); 178 | $tag_index = $tag_crawler->attr('index'); 179 | $tag_count = $tag_crawler->attr('count'); 180 | $tag_writable = $tag_crawler->attr('writable'); 181 | $writable = strtoupper($tag_writable) === "TRUE"; 182 | 183 | $g0 = $tag_g0 ?: $table_g0; 184 | $g1 = $tag_g1 ?: $table_g1; 185 | $g2 = $tag_g2 ?: $table_g2; 186 | /* 187 | 188 | if (!is_null($tag_g0)) { 189 | $extra['local_g0'] = $tag_g0; 190 | } 191 | 192 | if (!is_null($tag_g1) && !in_array($tag_g1, ['MakerNotes', 'Chapter#'])) { 193 | $group_name = $tag_g1; 194 | $extra['local_g1'] = $tag_g1; 195 | } 196 | else { 197 | $group_name = $table_g1; 198 | } 199 | 200 | if (!is_null($tag_g2)) { 201 | $extra['local_g2'] = $tag_g2; 202 | } 203 | */ 204 | 205 | $tag_flags = strtolower(trim($tag_crawler->attr('flags') ?: '')); 206 | $flags = $tag_flags ? explode(',', $tag_flags) : []; 207 | 208 | // $namespace = $table_name . '\\ID' . $tag_id; 209 | // if (!is_null($tag_index)) { 210 | // $namespace .= "\\v" . $tag_index; 211 | // } 212 | // $namespace = str_replace('::', '\\', $namespace); 213 | 214 | // first level namespace 215 | // $tn = explode('::', $table_name); 216 | // $prefix_ns = self::escapeClassname($tn[0]); 217 | 218 | // $prefix_ns = self::escapeClassname($g1); 219 | $prefix_ns = ''; 220 | // $prefix_ns = self::escapeClassname($table_name); 221 | // $prefix_ns = self::escapeClassname(strtoupper($tag_name[0])); 222 | // $namespace = self::escapeClassname("ID-" . $tag_id); 223 | $group_id = $g1 . ":" . $tag_name; 224 | $fq_classname = self::tagGroupIdToFQClassname($group_id); 225 | [$namespace, $classname] = self::fqClassnameToNamespace($fq_classname); 226 | 227 | // $fq_classname = $prefix_ns . '\\' . $namespace . '\\' . $classname; // fully qualified classname 228 | // $fq_classname = $namespace . '\\' . $classname; // fully qualified classname 229 | 230 | // tags with the same id+name reference the same "data" from a client point of vue. 231 | // so we group those into a tag group 232 | // $group_id = "ID-" . $tag_id . ":" . $tag_name; 233 | if (!array_key_exists($fq_classname, $tagGroupBuilders)) { 234 | 235 | // check that our dispatching method does not build 2 classes for one 236 | // this is NOW impossible (same key), but useful with other dispatch algo. 237 | if (array_key_exists($group_id, $group_ids)) { 238 | $this->logger->alert(sprintf("! GROUP_ID \"%s\" from \"%s\" already exists in \"%s\"", $group_id, $fq_classname, $group_ids[$group_id])); 239 | } 240 | else { 241 | $group_ids[$group_id] = $fq_classname; 242 | } 243 | 244 | $this->logger->info(sprintf("building \"%s\"", $fq_classname)); 245 | 246 | $tagGroupBuilder = new tagGroupBuilder( 247 | $this->rootNamespace, 248 | $namespace, 249 | $classname, 250 | // consts 251 | [ 252 | ], 253 | // tagProperties 254 | [ 255 | 'id' => $group_id, // used as full tagname for write ops 256 | 'name' => $tag_name, 257 | // 'type' => $tag_type, 258 | // 'php_type' => $php_type, 259 | // 'tags' => [], 260 | ], 261 | 'AbstractTagGroup', 262 | // uses 263 | [ 264 | 'JMS\\Serializer\\Annotation\\ExclusionPolicy', 265 | '\\PHPExiftool\\Driver\\AbstractTagGroup' 266 | ], 267 | // annotations 268 | [ 269 | '@ExclusionPolicy("all")' 270 | ] 271 | ); 272 | 273 | $tagGroupBuilder->setLogger($this->logger); 274 | 275 | $tagGroupBuilders[$fq_classname] = $tagGroupBuilder; 276 | } 277 | 278 | $tagComments = [ 279 | 'table_name' => $table_name, 280 | 'line' => $tag->getLineNo(), 281 | 'type' => $tag_type, 282 | 'writable' => $tag_writable, 283 | 'count' => $tag_count, 284 | 'flags' => $tag_flags, 285 | ]; 286 | 287 | $tagProperties = [ 288 | //'UKey' => $fq_classname, 289 | 'id' => $table_name . '.' . $group_id, 290 | ]; 291 | 292 | 293 | // keep "descriptions" on a per-tag level (no high level reconcilaiation) 294 | $tagDescriptions = []; 295 | foreach ($tag_crawler->filter('desc') as $desc) { 296 | $descCrawler = new Crawler($desc); 297 | $lng = $descCrawler->attr('lang'); 298 | if (in_array($lng, $lngs)) { 299 | $tagDescriptions[$lng] = $descCrawler->text(); 300 | } 301 | } 302 | $tagProperties['desc'] = $tagDescriptions; 303 | 304 | 305 | /* values is a mess with conflicting sense... don't try to use for now 306 | * 307 | // add "suggested values" to the top-level by merging values of each tag 308 | if (count($tag_crawler->filter('values')) > 0) { 309 | $values = []; 310 | 311 | $values_tag = $tag_crawler->filter('values')->first(); 312 | 313 | $Keys = $values_tag->filter('key'); 314 | 315 | foreach ($Keys as $Key) { 316 | $KeyCrawler = new Crawler(); 317 | $KeyCrawler->addNode($Key); 318 | 319 | $Id = $KeyCrawler->attr('id'); 320 | $Label = $KeyCrawler->filter('val[lang="en"]')->first()->text(); 321 | 322 | $values[$Id] = ['Id' => $Id, 'Label' => $Label]; 323 | } 324 | 325 | $tagProperties['Values'] = $values; 326 | } 327 | */ 328 | 329 | // now add the tag to the group 330 | $tagGroupBuilders[$fq_classname]->addTag( 331 | $tagComments, 332 | $tagProperties 333 | ); 334 | 335 | // and try to reconciliate some attributes 336 | 337 | // set a type and a php type at class level (will try to reconciliate) 338 | $tagGroupBuilders[$fq_classname]->setType($tag_type, $this->getPhpType($tag_type)); 339 | 340 | // set a writable flag at class level (will try to reconciliate) 341 | $tagGroupBuilders[$fq_classname]->setWritable($writable); 342 | 343 | // set a "count" attribute at class level (will try to reconciliate) 344 | if (!is_null($tag_count)) { 345 | $tagGroupBuilders[$fq_classname]->setCount((int)$tag_count); 346 | } 347 | 348 | // set a flag (named bool) attribute at class level (will try to reconciliate) 349 | $tagGroupBuilders[$fq_classname]->setFlags($flags); 350 | 351 | // set a common description (by lng) (will try to reconciliate) 352 | $tagGroupBuilders[$fq_classname]->setDescription($tagDescriptions); 353 | 354 | /* 355 | // set a description at class level (will try to reconciliate) 356 | / ** @var DOMElement $desc * / 357 | foreach($tag_crawler->filter('desc') as $desc) { 358 | $lng = $desc->getAttribute('lang'); 359 | $tagGroupBuilders[$fq_classname]->addDescription($lng, (string) $desc->textContent); 360 | } 361 | */ 362 | 363 | $nTags++; 364 | } 365 | } 366 | 367 | foreach ($tagGroupBuilders as $fq_classname => $builder) { 368 | $builder->computeProperties(); 369 | if($callback) { 370 | $callback($fq_classname, $builder); 371 | } 372 | } 373 | } 374 | 375 | protected function getPhpType(string $type): ?string 376 | { 377 | /** 378 | * Some of these types are described here: 379 | * http://trac.greenstone.org/browser/main/trunk/greenstone2/perllib/cpan/Image/ExifTool/README 380 | * http://cpansearch.perl.org/src/EXIFTOOL/Image-ExifTool-9.13/lib/Image/ExifTool/PICT.pm 381 | */ 382 | switch ($type) { 383 | # Formats defined in the wiki 384 | case 'int8s': 385 | case 'int8u': 386 | case 'int16s': 387 | case 'int16u': 388 | case 'int16uRev': 389 | case 'int32s': 390 | case 'int32u': 391 | case 'int32uRev': 392 | case 'int64s': 393 | case 'int64u': 394 | case 'fixed16s': 395 | case 'fixed32s': 396 | case 'fixed32u': 397 | case 'var_int16u': 398 | 399 | # Apple data structures in PICT images 400 | case 'Int8uText': 401 | case 'Int8u2Text': 402 | case 'Int16Data': 403 | case 'Int32uData': 404 | 405 | # Source unknown ... 406 | case 'var_int8u': 407 | case 'integer': 408 | case 'digits': 409 | case 'signed': 410 | case 'unsigned': 411 | return 'int'; 412 | 413 | 414 | # Formats defined in the wiki 415 | case 'float': 416 | case 'double': 417 | case 'extended': 418 | case 'rational32s': 419 | case 'rational32u': 420 | case 'rational64s': 421 | case 'rational64u': 422 | case 'rational': 423 | case 'real': 424 | return 'float'; 425 | 426 | 427 | # Formats defined in the wiki 428 | case 'undef': 429 | case 'binary': 430 | 431 | # Source unknown ... 432 | case 'var_ue7': 433 | case 'struct': 434 | case 'var_undef': 435 | case '?': 436 | case 'null': 437 | case 'unknown': 438 | case 'Unknown': 439 | return null; 440 | 441 | 442 | # Formats defined in the wiki 443 | case 'string': 444 | case 'pstring': 445 | case 'var_string': 446 | case 'var_pstr32': 447 | case 'var_ustr32': 448 | case 'unicode': 449 | case 'Unicode': 450 | case 'GUID': 451 | case 'vt_filetime': 452 | 453 | # Apple data structures in PICT images 454 | case 'Arc': 455 | case 'BitsRect#': # version-depended 456 | case 'BitsRgn#': # version-depended 457 | case 'CompressedQuickTime': 458 | case 'DirectBitsRect': 459 | case 'DirectBitsRgn': 460 | case 'FontName': 461 | case 'PixPat': 462 | case 'Point': 463 | case 'PointText': 464 | case 'Polygon': 465 | case 'Rect': 466 | case 'RGBColor': 467 | case 'Rgn': 468 | case 'ShortLine': 469 | 470 | # Source unknown ... 471 | case 'lang-alt': 472 | case 'resize': 473 | case 'utf8': 474 | case '2': // ??? 475 | return 'string'; 476 | 477 | 478 | # Source unknown ... 479 | case 'date': 480 | return 'date'; 481 | 482 | # Source unknown ... 483 | case 'boolean': 484 | return 'boolean'; 485 | 486 | # changed to "mixed" when types conflicts ... 487 | case 'mixed': 488 | return 'mixed'; 489 | 490 | default: 491 | $this->logger->alert(sprintf("No type found for %s @%s", $type, $this->currentXmlLine)); 492 | break; 493 | } 494 | 495 | return '?'; 496 | } 497 | 498 | 499 | protected static array $reservedNames = [ 500 | 'abstract', 501 | 'and', 502 | 'array', 503 | 'as', 504 | 'bool', 505 | 'break', 506 | 'case', 507 | 'catch', 508 | 'class', 509 | 'clone', 510 | 'const', 511 | 'continue', 512 | 'declare', 513 | 'default', 514 | 'do', 515 | 'else', 516 | 'elseif', 517 | 'empty', 518 | 'enddeclare', 519 | 'endfor', 520 | 'endforeach', 521 | 'endif', 522 | 'endswitch', 523 | 'endwhile', 524 | 'extends', 525 | 'false', 526 | 'final', 527 | 'float', 528 | 'for', 529 | 'foreach', 530 | 'function', 531 | 'function', 532 | 'global', 533 | 'goto', 534 | 'if', 535 | 'implements', 536 | 'instanceof', 537 | 'int', 538 | 'interface', 539 | 'mixed', 540 | 'namespace', 541 | 'new', 542 | 'null', 543 | 'numeric', 544 | 'object', 545 | 'old_function', 546 | 'or', 547 | 'private', 548 | 'protected', 549 | 'public', 550 | 'resource', 551 | 'static', 552 | 'string', 553 | 'switch', 554 | 'throw', 555 | 'true', 556 | 'try', 557 | 'use', 558 | 'var', 559 | 'void', 560 | 'while', 561 | 'xor', 562 | 'yield', 563 | ]; 564 | 565 | /** 566 | * build a valid class name 567 | * 568 | * @param string $name 569 | * @return string 570 | */ 571 | public static function escapeClassname(string $name): string 572 | { 573 | $retval = preg_replace('/[\\W_]+/i', '_', $name); 574 | 575 | if (in_array(strtolower($retval), static::$reservedNames)) { 576 | $retval = $retval . '0'; 577 | } 578 | 579 | return ucfirst($retval); 580 | } 581 | 582 | /** 583 | * transforms a taggroup id to a fq (but not including constant root part (vendor)) class name 584 | * e.g. "foo:ba#r:for" --> "Foo\Ba_r\For0" 585 | * 586 | * @param string $id 587 | * @return string 588 | */ 589 | public static function tagGroupIdToFQClassname(string $id): string 590 | { 591 | $parts = array_map( 592 | function ($part) { return self::escapeClassname($part); }, 593 | explode(':', $id) 594 | ); 595 | return join('\\', $parts); 596 | } 597 | 598 | /** 599 | * split namespace and name from a fqn 600 | * e.g. "Foo\Bar\Baz" --> [ "Foo\Bar" , "Baz" ] 601 | * 602 | * @param string $fq 603 | * @return array 604 | * @throws Exception 605 | */ 606 | private static function fqClassnameToNamespace(string $fq): array 607 | { 608 | $parts = explode('\\', $fq); 609 | $name = array_pop($parts); // remove last (classname) 610 | if(!$name) { 611 | throw new \Exception(sprintf("Bad FQName \"%s\"", $fq)); 612 | } 613 | $fq = join('\\', $parts); 614 | return [$fq ?: '\\' , $name]; 615 | } 616 | 617 | } 618 | --------------------------------------------------------------------------------