Writer.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. <?php
  2. /**
  3. * This file is part of the PHPExiftool package.
  4. *
  5. * (c) Alchemy <support@alchemy.fr>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace PHPExiftool;
  11. use PHPExiftool\Driver\Metadata\MetadataBag;
  12. use PHPExiftool\Exception\InvalidArgumentException;
  13. use Psr\Log\LoggerInterface;
  14. /**
  15. * Exiftool Metadatas Writer, it will be used to write metadatas in files
  16. *
  17. * Example usage :
  18. *
  19. * $Writer = new Writer();
  20. *
  21. * $metadatas = new MetadataBag();
  22. * $metadata->add(new Metadata(new IPTC\ObjectName(), Value\Mono('Super title !')));
  23. *
  24. * //writes the metadatas to the file
  25. * $Writer->writes('image.jpg', $metadatas);
  26. *
  27. * //writes the metadatas to image_copied.jpg
  28. * $Writer->writes('image.jpg', $metadatas, 'image_copied.jpg');
  29. *
  30. * @todo implement remove partial content
  31. * @todo implement binary thumbnails
  32. * @todo implements stay_open
  33. */
  34. class Writer
  35. {
  36. const MODE_IPTC2XMP = 1;
  37. const MODE_IPTC2EXIF = 2;
  38. const MODE_EXIF2IPTC = 4;
  39. const MODE_EXIF2XMP = 8;
  40. const MODE_PDF2XMP = 16;
  41. const MODE_XMP2PDF = 32;
  42. const MODE_GPS2XMP = 64;
  43. const MODE_XMP2EXIF = 128;
  44. const MODE_XMP2IPTC = 256;
  45. const MODE_XMP2GPS = 512;
  46. const MODULE_MWG = 1;
  47. protected $mode;
  48. protected $modules;
  49. protected $erase;
  50. private $exiftool;
  51. private $eraseProfile;
  52. protected $timeout = 60;
  53. public function __construct(Exiftool $exiftool)
  54. {
  55. $this->exiftool = $exiftool;
  56. $this->reset();
  57. }
  58. public function setTimeout($timeout)
  59. {
  60. $this->timeout = $timeout;
  61. return $this;
  62. }
  63. public function reset()
  64. {
  65. $this->mode = 0;
  66. $this->modules = 0;
  67. $this->erase = false;
  68. $this->eraseProfile = false;
  69. return $this;
  70. }
  71. /**
  72. * Enable / Disable modes
  73. *
  74. * @param integer $mode One of the self::MODE_*
  75. * @param Boolean $active Enable or disable the mode
  76. * @return Writer
  77. */
  78. public function setMode($mode, $active)
  79. {
  80. if ($active) {
  81. $this->mode |= $mode;
  82. } else {
  83. $this->mode = $this->mode & ~$mode;
  84. }
  85. return $this;
  86. }
  87. /**
  88. * Return true if the mode is enabled
  89. *
  90. * @param integer $mode One of the self::MODE_*
  91. * @return Boolean True if the mode is enabled
  92. */
  93. public function isMode($mode)
  94. {
  95. return (boolean) ($this->mode & $mode);
  96. }
  97. /**
  98. * Enable / disable module.
  99. * There's currently only one module self::MODULE_MWG
  100. *
  101. * @param integer $module One of the self::MODULE_*
  102. * @param Boolean $active Enable or disable the module
  103. * @return Writer
  104. */
  105. public function setModule($module, $active)
  106. {
  107. if ($active) {
  108. $this->modules |= $module;
  109. } else {
  110. $this->modules = $this->modules & ~$module;
  111. }
  112. return $this;
  113. }
  114. /**
  115. * Return true if the module is enabled
  116. *
  117. * @param integer $module
  118. * @return boolean
  119. */
  120. public function hasModule($module)
  121. {
  122. return (boolean) ($this->modules & $module);
  123. }
  124. /**
  125. * If set to true, erase all metadatas before write
  126. *
  127. * @param Boolean $boolean Whether to erase metadata or not before writing.
  128. * @param Boolean $maintainICCProfile Whether to maintain or not ICC Profile in case of erasing metadata.
  129. */
  130. public function erase($boolean, $maintainICCProfile = false)
  131. {
  132. $this->erase = (boolean) $boolean;
  133. $this->eraseProfile = !$maintainICCProfile;
  134. }
  135. /**
  136. * copy metadatas from one file to another
  137. * both files must exists.
  138. *
  139. * @param string $file_src The input file
  140. * @param string $file_dest The input file
  141. *
  142. * @return int the number "write" operations, or null if exiftool returned nothing we understand
  143. * event for no-op (file unchanged), 1 is returned so the caller does not think the command failed.
  144. *
  145. * @throws InvalidArgumentException
  146. */
  147. public function copy($file_src, $file_dest)
  148. {
  149. if ( ! file_exists($file_src)) {
  150. throw new InvalidArgumentException(sprintf('src %s does not exists', $file_src));
  151. }
  152. if ( ! file_exists($file_dest)) {
  153. throw new InvalidArgumentException(sprintf('dest %s does not exists', $file_dest));
  154. }
  155. $command = "-overwrite_original_in_place -tagsFromFile " . escapeshellarg($file_src) . " " . escapeshellarg($file_dest);
  156. $ret = $this->exiftool->executeCommand($command, $this->timeout);
  157. // exiftool may print (return) a bunch of lines, even for a single command
  158. // eg. deleting tags of a file with NO tags may return 2 lines...
  159. // | exiftool -all:all= notags.jpg
  160. // | 0 image files updated
  161. // | 1 image files unchanged
  162. // ... which is NOT an error
  163. // so it's not easy to decide from the output when something went REALLY wrong
  164. $n_unchanged = $n_changed = 0;
  165. foreach(explode("\n", $ret) as $line) {
  166. if (preg_match("/(\\d+) image files (copied|created|updated|unchanged)/", $line, $matches)) {
  167. if($matches[2] == 'unchanged') {
  168. $n_unchanged += (int)($matches[1]);
  169. }
  170. else {
  171. $n_changed += (int)($matches[1]);
  172. }
  173. }
  174. }
  175. // first chance, changes happened
  176. if($n_changed > 0) {
  177. // return $n_changed;
  178. return 1; // so tests are ok
  179. }
  180. // second chance, at least one no-op happened
  181. if($n_unchanged > 0) {
  182. return 1;
  183. }
  184. // too bad
  185. return null;
  186. }
  187. /**
  188. * Writes metadatas to the file. If a destination is provided, original file
  189. * is not modified.
  190. *
  191. * @param string $file The input file
  192. * @param MetadataBag $metadatas A bag of metadatas
  193. * @param string $destination The output file
  194. * @param array $resolutionXY The dpi resolution array(xresolution, yresolution)
  195. *
  196. * @return int the number "write" operations, or null if exiftool returned nothing we understand
  197. * event for no-op (file unchanged), 1 is returned so the caller does not think the command failed.
  198. *
  199. * @throws InvalidArgumentException
  200. */
  201. public function write($file, MetadataBag $metadatas, $destination = null, array $resolutionXY = array())
  202. {
  203. if ( ! file_exists($file)) {
  204. throw new InvalidArgumentException(sprintf('%s does not exists', $file));
  205. }
  206. // if the -o file exists, exiftool prints an error
  207. if($destination) {
  208. @unlink($destination);
  209. if (file_exists($destination)) {
  210. throw new InvalidArgumentException(sprintf('%s cannot be replaced', $destination));
  211. }
  212. }
  213. $common_args = '-ignoreMinorErrors -preserve -charset UTF8';
  214. $commands = array();
  215. if ($this->erase) {
  216. /**
  217. * if erase is specfied, we MUST start by erasing datas before doing
  218. * anything else.
  219. */
  220. $commands[] = '-all:all= ' . ($this->eraseProfile ? '' : '--icc_profile:all') ;
  221. }
  222. if(count($resolutionXY) == 2 && is_int(current($resolutionXY)) && is_int(end($resolutionXY)) ){
  223. reset($resolutionXY);
  224. $commands[] = '-xresolution=' . current($resolutionXY) . ' -yresolution=' . end($resolutionXY);
  225. }
  226. if(count($metadatas) > 0) {
  227. $commands[] = $this->addMetadatasArg($metadatas);
  228. $common_args .= ' -codedcharacterset=utf8';
  229. }
  230. if ('' !== ($syncCommand = $this->getSyncCommand())) {
  231. $commands[] = $syncCommand;
  232. }
  233. if(count($commands) == 0) {
  234. // nothing to do...
  235. if($destination) {
  236. // ... but a destination
  237. $commands[] = ''; // empty command so exiftool will copy the file for us
  238. }
  239. else {
  240. // really nothing to do = 0 ops
  241. return 1; // considered a "unchnanged"
  242. }
  243. }
  244. if($destination) {
  245. foreach($commands as $i=>$command) {
  246. if($i==0) {
  247. // the FIRST command will -o the destination
  248. $commands[0] .= ' ' . $file . ' -o ' . $destination;
  249. }
  250. else {
  251. // then the next commands will work on the destination
  252. $commands[$i] .= ' -overwrite_original_in_place ' . $destination;
  253. }
  254. }
  255. }
  256. else {
  257. // every command (even a single one) work on the original file
  258. $common_args .= ' -overwrite_original_in_place ' . $file;
  259. }
  260. if(count($commands) > 1) {
  261. // really need "-common_args" only if many commands are chained
  262. // nb: the file argument CAN be into -common_args
  263. $common_args = '-common_args ' . $common_args;
  264. }
  265. $command = join(" -execute ", $commands) . ' ' . $common_args;
  266. $ret = $this->exiftool->executeCommand($command, $this->timeout);
  267. // exiftool may print (return) a bunch of lines, even for a single command
  268. // eg. deleting tags of a file with NO tags may return 2 lines...
  269. // | exiftool -all:all= notags.jpg
  270. // | 0 image files updated
  271. // | 1 image files unchanged
  272. // ... which is NOT an error
  273. // so it's not easy to decide from the output when something went REALLY wrong
  274. $n_unchanged = $n_changed = 0;
  275. foreach(explode("\n", $ret) as $line) {
  276. if (preg_match("/(\\d+) image files (copied|created|updated|unchanged)/", $line, $matches)) {
  277. if($matches[2] == 'unchanged') {
  278. $n_unchanged += (int)($matches[1]);
  279. }
  280. else {
  281. $n_changed += (int)($matches[1]);
  282. }
  283. }
  284. }
  285. // first chance, changes happened
  286. if($n_changed > 0) {
  287. // return $n_changed; // nice but breaks backward compatibility
  288. return 1; // better, backward compatible and tests are ok
  289. }
  290. // second chance, at least one no-op happened
  291. if($n_unchanged > 0) {
  292. return 1;
  293. }
  294. // too bad
  295. return null;
  296. }
  297. /**
  298. * Factory for standard Writer
  299. *
  300. * @return Writer
  301. */
  302. public static function create(LoggerInterface $logger)
  303. {
  304. return new Writer(new Exiftool($logger));
  305. }
  306. /**
  307. * Computes modes, modules and metadatas to a single commandline
  308. *
  309. * @param MetadataBag $metadatas A Bag of metadatas
  310. *
  311. * @return string A part of the command
  312. */
  313. protected function addMetadatasArg(MetadataBag $metadatas)
  314. {
  315. $command = '';
  316. if ($this->modules & self::MODULE_MWG) {
  317. $command .= '-use MWG';
  318. }
  319. foreach ($metadatas as $metadata) {
  320. foreach ($metadata->getValue()->asArray() as $value) {
  321. $command .= ($command ? ' -' : '-') . $metadata->getTag()->getTagname() . '='
  322. . escapeshellarg($value);
  323. }
  324. }
  325. return $command;
  326. }
  327. protected function getSyncCommand()
  328. {
  329. $syncCommand = '';
  330. $availableArgs = array(
  331. self::MODE_IPTC2XMP => 'iptc2xmp.args',
  332. self::MODE_IPTC2EXIF => 'iptc2exif.args',
  333. self::MODE_EXIF2IPTC => 'exif2iptc.args',
  334. self::MODE_EXIF2XMP => 'exif2xmp.args',
  335. self::MODE_PDF2XMP => 'pdf2xmp.args',
  336. self::MODE_XMP2PDF => 'xmp2pdf.args',
  337. self::MODE_GPS2XMP => 'gps2xmp.args',
  338. self::MODE_XMP2EXIF => 'xmp2exif.args',
  339. self::MODE_XMP2IPTC => 'xmp2iptc.args',
  340. self::MODE_XMP2GPS => 'xmp2gps.args',
  341. );
  342. foreach ($availableArgs as $arg => $cmd) {
  343. if ($this->mode & $arg) {
  344. $syncCommand .= ($syncCommand ? ' -@ ' : '-@ ') . $cmd;
  345. }
  346. }
  347. return $syncCommand;
  348. }
  349. }