tikiwiki/packages/tiki-pkg-mediaalchemyst/alchemy/phpexiftool/lib/PHPExiftool/Writer.php

394 lines
12 KiB
PHP
Raw Normal View History

2023-11-20 21:52:04 +01:00
<?php
/**
* This file is part of the PHPExiftool package.
*
* (c) Alchemy <support@alchemy.fr>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPExiftool;
use PHPExiftool\Driver\Metadata\MetadataBag;
use PHPExiftool\Exception\InvalidArgumentException;
use Psr\Log\LoggerInterface;
/**
* Exiftool Metadatas Writer, it will be used to write metadatas in files
*
* Example usage :
*
* $Writer = new Writer();
*
* $metadatas = new MetadataBag();
* $metadata->add(new Metadata(new IPTC\ObjectName(), Value\Mono('Super title !')));
*
* //writes the metadatas to the file
* $Writer->writes('image.jpg', $metadatas);
*
* //writes the metadatas to image_copied.jpg
* $Writer->writes('image.jpg', $metadatas, 'image_copied.jpg');
*
* @todo implement remove partial content
* @todo implement binary thumbnails
* @todo implements stay_open
*/
class Writer
{
const MODE_IPTC2XMP = 1;
const MODE_IPTC2EXIF = 2;
const MODE_EXIF2IPTC = 4;
const MODE_EXIF2XMP = 8;
const MODE_PDF2XMP = 16;
const MODE_XMP2PDF = 32;
const MODE_GPS2XMP = 64;
const MODE_XMP2EXIF = 128;
const MODE_XMP2IPTC = 256;
const MODE_XMP2GPS = 512;
const MODULE_MWG = 1;
protected $mode;
protected $modules;
protected $erase;
private $exiftool;
private $eraseProfile;
protected $timeout = 60;
public function __construct(Exiftool $exiftool)
{
$this->exiftool = $exiftool;
$this->reset();
}
public function setTimeout($timeout)
{
$this->timeout = $timeout;
return $this;
}
public function reset()
{
$this->mode = 0;
$this->modules = 0;
$this->erase = false;
$this->eraseProfile = false;
return $this;
}
/**
* Enable / Disable modes
*
* @param integer $mode One of the self::MODE_*
* @param Boolean $active Enable or disable the mode
* @return Writer
*/
public function setMode($mode, $active)
{
if ($active) {
$this->mode |= $mode;
} else {
$this->mode = $this->mode & ~$mode;
}
return $this;
}
/**
* Return true if the mode is enabled
*
* @param integer $mode One of the self::MODE_*
* @return Boolean True if the mode is enabled
*/
public function isMode($mode)
{
return (boolean) ($this->mode & $mode);
}
/**
* Enable / disable module.
* There's currently only one module self::MODULE_MWG
*
* @param integer $module One of the self::MODULE_*
* @param Boolean $active Enable or disable the module
* @return Writer
*/
public function setModule($module, $active)
{
if ($active) {
$this->modules |= $module;
} else {
$this->modules = $this->modules & ~$module;
}
return $this;
}
/**
* Return true if the module is enabled
*
* @param integer $module
* @return boolean
*/
public function hasModule($module)
{
return (boolean) ($this->modules & $module);
}
/**
* If set to true, erase all metadatas before write
*
* @param Boolean $boolean Whether to erase metadata or not before writing.
* @param Boolean $maintainICCProfile Whether to maintain or not ICC Profile in case of erasing metadata.
*/
public function erase($boolean, $maintainICCProfile = false)
{
$this->erase = (boolean) $boolean;
$this->eraseProfile = !$maintainICCProfile;
}
/**
* copy metadatas from one file to another
* both files must exists.
*
* @param string $file_src The input file
* @param string $file_dest The input file
*
* @return int the number "write" operations, or null if exiftool returned nothing we understand
* event for no-op (file unchanged), 1 is returned so the caller does not think the command failed.
*
* @throws InvalidArgumentException
*/
public function copy($file_src, $file_dest)
{
if ( ! file_exists($file_src)) {
throw new InvalidArgumentException(sprintf('src %s does not exists', $file_src));
}
if ( ! file_exists($file_dest)) {
throw new InvalidArgumentException(sprintf('dest %s does not exists', $file_dest));
}
$command = "-overwrite_original_in_place -tagsFromFile " . escapeshellarg($file_src) . " " . escapeshellarg($file_dest);
$ret = $this->exiftool->executeCommand($command, $this->timeout);
// exiftool may print (return) a bunch of lines, even for a single command
// eg. deleting tags of a file with NO tags may return 2 lines...
// | exiftool -all:all= notags.jpg
// | 0 image files updated
// | 1 image files unchanged
// ... which is NOT an error
// so it's not easy to decide from the output when something went REALLY wrong
$n_unchanged = $n_changed = 0;
foreach(explode("\n", $ret) as $line) {
if (preg_match("/(\\d+) image files (copied|created|updated|unchanged)/", $line, $matches)) {
if($matches[2] == 'unchanged') {
$n_unchanged += (int)($matches[1]);
}
else {
$n_changed += (int)($matches[1]);
}
}
}
// first chance, changes happened
if($n_changed > 0) {
// return $n_changed;
return 1; // so tests are ok
}
// second chance, at least one no-op happened
if($n_unchanged > 0) {
return 1;
}
// too bad
return null;
}
/**
* Writes metadatas to the file. If a destination is provided, original file
* is not modified.
*
* @param string $file The input file
* @param MetadataBag $metadatas A bag of metadatas
* @param string $destination The output file
* @param array $resolutionXY The dpi resolution array(xresolution, yresolution)
*
* @return int the number "write" operations, or null if exiftool returned nothing we understand
* event for no-op (file unchanged), 1 is returned so the caller does not think the command failed.
*
* @throws InvalidArgumentException
*/
public function write($file, MetadataBag $metadatas, $destination = null, array $resolutionXY = array())
{
if ( ! file_exists($file)) {
throw new InvalidArgumentException(sprintf('%s does not exists', $file));
}
// if the -o file exists, exiftool prints an error
if($destination) {
@unlink($destination);
if (file_exists($destination)) {
throw new InvalidArgumentException(sprintf('%s cannot be replaced', $destination));
}
}
$common_args = '-ignoreMinorErrors -preserve -charset UTF8';
$commands = array();
if ($this->erase) {
/**
* if erase is specfied, we MUST start by erasing datas before doing
* anything else.
*/
$commands[] = '-all:all= ' . ($this->eraseProfile ? '' : '--icc_profile:all') ;
}
if(count($resolutionXY) == 2 && is_int(current($resolutionXY)) && is_int(end($resolutionXY)) ){
reset($resolutionXY);
$commands[] = '-xresolution=' . current($resolutionXY) . ' -yresolution=' . end($resolutionXY);
}
if(count($metadatas) > 0) {
$commands[] = $this->addMetadatasArg($metadatas);
$common_args .= ' -codedcharacterset=utf8';
}
if ('' !== ($syncCommand = $this->getSyncCommand())) {
$commands[] = $syncCommand;
}
if(count($commands) == 0) {
// nothing to do...
if($destination) {
// ... but a destination
$commands[] = ''; // empty command so exiftool will copy the file for us
}
else {
// really nothing to do = 0 ops
return 1; // considered a "unchnanged"
}
}
if($destination) {
foreach($commands as $i=>$command) {
if($i==0) {
// the FIRST command will -o the destination
$commands[0] .= ' ' . $file . ' -o ' . $destination;
}
else {
// then the next commands will work on the destination
$commands[$i] .= ' -overwrite_original_in_place ' . $destination;
}
}
}
else {
// every command (even a single one) work on the original file
$common_args .= ' -overwrite_original_in_place ' . $file;
}
if(count($commands) > 1) {
// really need "-common_args" only if many commands are chained
// nb: the file argument CAN be into -common_args
$common_args = '-common_args ' . $common_args;
}
$command = join(" -execute ", $commands) . ' ' . $common_args;
$ret = $this->exiftool->executeCommand($command, $this->timeout);
// exiftool may print (return) a bunch of lines, even for a single command
// eg. deleting tags of a file with NO tags may return 2 lines...
// | exiftool -all:all= notags.jpg
// | 0 image files updated
// | 1 image files unchanged
// ... which is NOT an error
// so it's not easy to decide from the output when something went REALLY wrong
$n_unchanged = $n_changed = 0;
foreach(explode("\n", $ret) as $line) {
if (preg_match("/(\\d+) image files (copied|created|updated|unchanged)/", $line, $matches)) {
if($matches[2] == 'unchanged') {
$n_unchanged += (int)($matches[1]);
}
else {
$n_changed += (int)($matches[1]);
}
}
}
// first chance, changes happened
if($n_changed > 0) {
// return $n_changed; // nice but breaks backward compatibility
return 1; // better, backward compatible and tests are ok
}
// second chance, at least one no-op happened
if($n_unchanged > 0) {
return 1;
}
// too bad
return null;
}
/**
* Factory for standard Writer
*
* @return Writer
*/
public static function create(LoggerInterface $logger)
{
return new Writer(new Exiftool($logger));
}
/**
* Computes modes, modules and metadatas to a single commandline
*
* @param MetadataBag $metadatas A Bag of metadatas
*
* @return string A part of the command
*/
protected function addMetadatasArg(MetadataBag $metadatas)
{
$command = '';
if ($this->modules & self::MODULE_MWG) {
$command .= '-use MWG';
}
foreach ($metadatas as $metadata) {
foreach ($metadata->getValue()->asArray() as $value) {
$command .= ($command ? ' -' : '-') . $metadata->getTag()->getTagname() . '='
. escapeshellarg($value);
}
}
return $command;
}
protected function getSyncCommand()
{
$syncCommand = '';
$availableArgs = array(
self::MODE_IPTC2XMP => 'iptc2xmp.args',
self::MODE_IPTC2EXIF => 'iptc2exif.args',
self::MODE_EXIF2IPTC => 'exif2iptc.args',
self::MODE_EXIF2XMP => 'exif2xmp.args',
self::MODE_PDF2XMP => 'pdf2xmp.args',
self::MODE_XMP2PDF => 'xmp2pdf.args',
self::MODE_GPS2XMP => 'gps2xmp.args',
self::MODE_XMP2EXIF => 'xmp2exif.args',
self::MODE_XMP2IPTC => 'xmp2iptc.args',
self::MODE_XMP2GPS => 'xmp2gps.args',
);
foreach ($availableArgs as $arg => $cmd) {
if ($this->mode & $arg) {
$syncCommand .= ($syncCommand ? ' -@ ' : '-@ ') . $cmd;
}
}
return $syncCommand;
}
}