├── 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 | [](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 |
--------------------------------------------------------------------------------