├── README.md ├── composer.json └── src ├── Exception └── Error.php └── Package.php /README.md: -------------------------------------------------------------------------------- 1 | ![PRs welcome but not actively maintained](https://img.shields.io/badge/status-PRs%20welcome%20but%20not%20actively%20maintained-red.svg?style=flat-square) 2 | 3 | ## php-idml 4 | 5 | A simple class to handle file management of Adobe InDesign IDML files. Keeping track of all the individual files within 6 | an `.idml` package is a chore. This helps a bit. (A *bit*.) 7 | 8 | ### Installation 9 | 10 | composer require prometee/php-idml 11 | 12 | #### Usage 13 | 14 | Instantiate the object: 15 | 16 | require __DIR__.'/../vendor/autoload.php'; 17 | $idml = new \IDML\Package(); 18 | $idml->setZip("filename.idml") 19 | $idml->load(); 20 | 21 | This will unzip `filename.idml` to a directory called `.filename.idml`. This directory will be deleted when the object 22 | is garbage-collected (see the `__destruct()` method). Alternatively, if you keep your IDMLs stored unzipped, a directory 23 | can also be passed to the constructor: 24 | 25 | require __DIR__.'/../vendor/autoload.php'; 26 | $idml = new \IDML\Package(); 27 | $idml->setDirectory("/path/to/idml/"); 28 | $idml->load(); 29 | 30 | This directory will not be deleted upon object destruction. (Admittedly this is not a typical use-case but happened to 31 | be how the project I was working on stored things.) 32 | 33 | The `IDML\Package` object is essentially a file manager/server of `DOMDocument` objects for all the internal files. 34 | That's most of what this class does. 35 | 36 | $spreads = $idml->getSpreads(); 37 | $storyu12a = $idml->getStorie("u12a"); 38 | $backingStory = $idml->getBackingStory(); 39 | // etc 40 | 41 | Generally if you want Whatever, use `getWhatever()`. You likely won't need to use any of the setters but they're there 42 | if you want to do something weird. Note `getSpread()` is a convenience method that gets the first spread, operating 43 | under the assumption that there is only one. This applied to the project from which this class was born, but may not 44 | apply to yours. This method is also used in the `addElementToSpread()` method if a spread is not explicitly provided. 45 | Use caution. 46 | 47 | #### Notable Methods 48 | 49 | These are just the most generic methods that existed in the project-specific version of this class. I have put zero 50 | effort into trying to make this a generically useful class. 51 | 52 | - `getLayers($selfsOnly, $visibleOnly)`: Returns an array of layers from the `designmap.xml` of this IDML. It can either 53 | return the layer elements (`DOMNodes`) or the self attributes (e.g. "u12a") of the layers. The `$visibleOnly` flag 54 | determines whether non-visible layers should be included. 55 | - `getElementBySelfAttribute($self)`: Returns the element identified by `$self` from whatever file it lives in. So if 56 | you're looking for `` but don't know if it's in a spread or a story, use this. Throws if 57 | it fails but I can't imagine you'd be looking for an element by using an arbitrary self attribute. If you do, wrap 58 | this call in a `try/catch` block. 59 | - `addStoryToDesignMap($val)`: If you've created a new story, this is a quick method to add your story to the package 60 | and put it in the `designmap.xml` so InDesign can find it. `$val` needs to be a `DOMDocument`. 61 | - `getAppliedStyle($node)`: given a `CharacterStyleRange` or `ParagraphStyleRange` element, this method will return the 62 | associated `CharacterStyle` or `ParagraphStyle` node from the `Styles.xml` file. 63 | - `getStyleAttribute($node, $attr)`: given a `CharacterStyleRange` or `ParagraphStyleRange` node and the name of an 64 | attribute, this method will return the value of that attribute as thoroughly as possible by checking the node, its 65 | applied style node, and its parent and parent's applied style nodes (if applicable). 66 | - `getStyleProperty($node, $prop)`: the same as `getStyleAttribute()` but for properties rather than attributes. 67 | - `getMarkupTag($node)`: Get the tag associated with a given element (`DOMNode`) - or an ancestor. It searches every 68 | element from `$node` up and returns the first tag it finds. This logic may not exactly jibe with how tags are intended 69 | to work in IDML; it does what I need it to do for my project but may not work for you. The tag it returns is 70 | `urldecode()`ed. 71 | - `saveAll($zip_file_path)`: Save all the files in the IDML. There are individual save methods for saving various pieces but why 72 | bother? Just use this one. 73 | 74 | Everything else you'll just have to figure out on your own. 75 | 76 | #### License 77 | 78 | MIT 79 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prometee/php-idml", 3 | "description": "A simple class to handle file management of Adobe InDesign IDML files.", 4 | "type": "library", 5 | "license": "MIT", 6 | "repositories": [ 7 | { 8 | "type": "git", 9 | "url": "https://github.com/Prometee/php-idml.git" 10 | } 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Francis Hilaire", 15 | "email": "prometee@gmail.com", 16 | "homepage": "http://francishilaire.fr" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=5.4.0", 21 | "ext-zip": "*" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "IDML\\": "src" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Exception/Error.php: -------------------------------------------------------------------------------- 1 | unzip_path && $this->zip) { 127 | $this->unzip_path = dirname($this->zip) . DIRECTORY_SEPARATOR . "." . basename($this->zip); 128 | } 129 | return $this->unzip_path; 130 | } 131 | 132 | /** 133 | * @param string $unzip_path 134 | * @return $this 135 | */ 136 | public function setUnzipPath($unzip_path) 137 | { 138 | $this->unzip_path = rtrim($unzip_path, '/').'/'; 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * Return the full complete paths of the entire contents of a directory including all subdirectories. 145 | * @param string $path The path of the directory whose contents you want. 146 | * @return array An array of the full paths of those contents. 147 | */ 148 | // this is included here to make this class standalone but ideally you'd factor it out 149 | // see my gist of recursive utils @ https://gist.github.com/deathlyfrantic/086bba7b2a25ec8e57cc 150 | public function getDirectoryContents($path) { 151 | $results = []; 152 | 153 | try { 154 | $iterator = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS); 155 | foreach ($iterator as $i) { 156 | $results[] = $i; 157 | if ($i->isDir()) { 158 | $results = array_merge($results, $this->getDirectoryContents($i)); 159 | } 160 | } 161 | } catch (UnexpectedValueException $e) { 162 | // $results is already an empty array so nothing to do here, we'll just return it as is. 163 | } 164 | 165 | return $results; 166 | } 167 | 168 | /** 169 | * This is an array used to map package elements to their respective load methods. 170 | * See $this->load(). I am lazy and hate typing. 171 | * 172 | * @var array 173 | */ 174 | protected $packageElements = [ 175 | "BackingStory" => "set", 176 | "Fonts" => "set", 177 | "Graphic" => "set", 178 | "Mapping" => "set", 179 | "MasterSpread" => "add", 180 | "Preferences" => "set", 181 | "Spread" => "add", 182 | "Story" => "add", 183 | "Styles" => "set", 184 | "Tags" => "set" 185 | ]; 186 | 187 | /** 188 | * If the parameter is an IDML file - and there is not a ton of checking that goes into that - 189 | * pass it to setZip() which unzips it and loads up stuff. Otherwise, if it's a directory, 190 | * load it from that. 191 | * The project I developed this for uses IDML packages that live in an unzipped form 192 | * so they can be easily-modified before zipping up for processing. 193 | * 194 | * @param string|null $path 195 | */ 196 | public function __construct($path = null) { 197 | if ($path) { 198 | if (preg_match("#".self::IDML_FILENAME_EXTENSION."$#i", $path)) { 199 | $this 200 | ->setZip($path) 201 | ->load(); 202 | } else if (is_dir($path)) { 203 | $this 204 | ->setDirectory($path) 205 | ->load(); 206 | } 207 | } 208 | } 209 | 210 | /** 211 | * Basically if this package was loaded from an IDML file, we're assuming we unzipped it, 212 | * so delete the temp directory and all the files we created by unzipping it. 213 | */ 214 | public function __destruct() { 215 | if ($this->isZip() && is_dir($this->getDirectory())) { 216 | $pathObjects = $this->getDirectoryContents($this->getDirectory()); 217 | 218 | $contents = array_map( 219 | function (SplFileInfo $pathObject) { 220 | return $pathObject->getRealPath(); 221 | }, 222 | array_reverse($pathObjects) 223 | ); 224 | 225 | foreach ($contents as $content) { 226 | if (is_dir($content)) { 227 | rmdir($content); 228 | } else { 229 | unlink($content); 230 | } 231 | } 232 | 233 | rmdir($this->getDirectory()); 234 | } 235 | } 236 | 237 | /** 238 | * Get the design map of the IDML. 239 | * @return DOMDocument The designmap.xml of the IDML package. 240 | */ 241 | public function getDesignMap() { 242 | return $this->designMap; 243 | } 244 | 245 | /** 246 | * Set the design map of the IDML. 247 | * @param DOMDocument $val The DOMDocument object loaded with the designmap.xml file. 248 | * @return $this 249 | */ 250 | public function setDesignMap(DOMDocument $val) { 251 | $this->designMap = $val; 252 | return $this; 253 | } 254 | 255 | /** 256 | * Get a single master spread. 257 | * @param $key 258 | * @return DOMElement|null 259 | */ 260 | public function getMasterSpread($key) { 261 | if (array_key_exists($key, $this->masterSpreads)) { 262 | return $this->masterSpreads[$key]; 263 | } 264 | 265 | return null; 266 | } 267 | 268 | /** 269 | * @return DOMElement[] 270 | */ 271 | public function getMasterSpreads() { 272 | return $this->masterSpreads; 273 | } 274 | 275 | /** 276 | * Master spreads setter. If you only have one, wrap it in [] before passing. 277 | * @param array $val The array of master spreads. 278 | * @return $this This object for method chaining. 279 | */ 280 | public function setMasterSpreads(array $val) { 281 | $this->designMap = $val; 282 | return $this; 283 | } 284 | 285 | /** 286 | * Returns the DOMDocument object of the Graphic.xml file. 287 | * @return DOMDocument 288 | */ 289 | public function getGraphic() { 290 | return $this->graphic; 291 | } 292 | 293 | /** 294 | * Graphic setter. 295 | * @param DOMDocument $val The Graphic.xml file's DOMDocument. 296 | * @return $this This object for method chaining. 297 | */ 298 | public function setGraphic(DOMDocument $val) { 299 | $this->graphic = $val; 300 | return $this; 301 | } 302 | 303 | /** 304 | * Returns the DOMDocument object of the Fonts.xml file. 305 | * @return DOMDocument 306 | */ 307 | public function getFonts() { 308 | return $this->fonts; 309 | } 310 | 311 | /** 312 | * Fonts setter. 313 | * @param DOMDocument $val The Fonts.xml file's DOMDocument. 314 | * @return $this This object for method chaining. 315 | */ 316 | public function setFonts(DOMDocument $val) { 317 | $this->fonts = $val; 318 | return $this; 319 | } 320 | 321 | /** 322 | * Returns the DOMDocument object of the Styles.xml file. 323 | * @return DOMDocument 324 | */ 325 | public function getStyles() { 326 | return $this->styles; 327 | } 328 | 329 | /** 330 | * Styles setter. 331 | * @param DOMDocument $val The Styles.xml file's DOMDocument. 332 | * @return $this This object for method chaining. 333 | */ 334 | public function setStyles(DOMDocument $val) { 335 | $this->styles = $val; 336 | return $this; 337 | } 338 | 339 | /** 340 | * Returns the DOMDocument object of the Preferences.xml file. 341 | * @return DOMDocument 342 | */ 343 | public function getPreferences() { 344 | return $this->preferences; 345 | } 346 | 347 | /** 348 | * Preferences setter. 349 | * @param DOMDocument $val The Preferences.xml file's DOMDocument. 350 | * @return $this This object for method chaining. 351 | */ 352 | public function setPreferences(DOMDocument $val) { 353 | $this->preferences = $val; 354 | return $this; 355 | } 356 | 357 | /** 358 | * @param $key 359 | * @return DOMElement|null 360 | */ 361 | public function getSpread($key) { 362 | if (array_key_exists($key, $this->spreads)) { 363 | return $this->spreads[$key]; 364 | } 365 | 366 | return null; 367 | } 368 | 369 | /** 370 | * @return DOMElement[] 371 | */ 372 | public function getSpreads() { 373 | return $this->spreads; 374 | } 375 | 376 | /** 377 | * Spreads setter. If you only have one, wrap it in [] before passing. 378 | * @param array $val The array of spreads. 379 | * @return $this This object for method chaining. 380 | */ 381 | public function setSpreads(array $val) { 382 | $this->spreads = $val; 383 | return $this; 384 | } 385 | 386 | /** 387 | * @param $key 388 | * @return DOMElement|null 389 | */ 390 | public function getStorie($key) { 391 | if (array_key_exists($key, $this->stories)) { 392 | return $this->stories[$key]; 393 | } 394 | 395 | return null; 396 | } 397 | 398 | /** 399 | * @return DOMElement[] 400 | */ 401 | public function getStories() { 402 | return $this->stories; 403 | } 404 | 405 | /** 406 | * Stories setter. If you only have one, wrap it in [] before passing. 407 | * @param array $val The array of stories. 408 | * @return $this This object for method chaining. 409 | */ 410 | public function setStories(array $val) { 411 | $this->stories = $val; 412 | return $this; 413 | } 414 | 415 | /** 416 | * Returns the DOMDocument object of the BackingStory.xml file. 417 | * @return DOMDocument 418 | */ 419 | public function getBackingStory() { 420 | return $this->backingStory; 421 | } 422 | 423 | /** 424 | * Backing story setter. 425 | * @param DOMDocument $val The BackingStory.xml file's DOMDocument. 426 | * @return $this This object for method chaining. 427 | */ 428 | public function setBackingStory(DOMDocument $val) { 429 | $this->backingStory = $val; 430 | return $this; 431 | } 432 | 433 | /** 434 | * Returns the DOMDocument object of the Tags.xml file. 435 | * @return DOMDocument 436 | */ 437 | public function getTags() { 438 | return $this->tags; 439 | } 440 | 441 | /** 442 | * Tags setter. 443 | * @param DOMDocument $val The Tags.xml file's DOMDocument. 444 | * @return $this This object for method chaining. 445 | */ 446 | public function setTags(DOMDocument $val) { 447 | $this->tags = $val; 448 | return $this; 449 | } 450 | 451 | /** 452 | * Returns the DOMDocument object of the Mapping.xml file. 453 | * @return DOMDocument 454 | */ 455 | public function getMapping() { 456 | return $this->mapping; 457 | } 458 | 459 | /** 460 | * Mapping setter. 461 | * @param DOMDocument $val The Mapping.xml file's DOMDocument. 462 | * @return $this This object for method chaining. 463 | */ 464 | public function setMapping(DOMDocument $val) { 465 | $this->mapping = $val; 466 | return $this; 467 | } 468 | 469 | /** 470 | * Set the directory for this IDML package. Basically if you have /directory/idmlfile.idml 471 | * and unzipped it, you'd setDirectory("/directory/"). 472 | * @param string $path 473 | * @return $this 474 | * @throws Error 475 | */ 476 | public function setDirectory($path) { 477 | if (is_dir($path)) { 478 | $this->directory = rtrim(realpath($path), '/').'/'; 479 | } else { 480 | throw new Error(sprintf('Directory not found : %s', $path)); 481 | } 482 | 483 | return $this; 484 | } 485 | 486 | /** 487 | * Returns a string containing the directory name of this IDML. 488 | * @return string 489 | */ 490 | public function getDirectory() { 491 | return $this->directory; 492 | } 493 | 494 | /** 495 | * Returns a string containing the zip file name of this IDML. 496 | * @return string 497 | */ 498 | public function getZip() { 499 | return $this->zip; 500 | } 501 | 502 | /** 503 | * Set the zip file for this package. If you have /directory/idmlfile.idml, you'd 504 | * setZip("/directory/idmlfile.idml"). This would then unzip that file to /directory/.idmlfile.idml/ 505 | * and load all the stuff. 506 | * The __destruct() method ensures this temporary directory will be deleted upon object destruction. 507 | * 508 | * @param $path string 509 | * @return $this 510 | * @throws Error 511 | */ 512 | public function setZip($path) { 513 | if (file_exists($path)) { 514 | $this->zip = realpath($path); 515 | return $this; 516 | } else { 517 | throw new Error(sprintf('File not found : %s', $path)); 518 | } 519 | } 520 | 521 | 522 | /** 523 | * @param string $zip_filename 524 | * @param string $unzip_path 525 | * @return bool 526 | * @throws Error 527 | */ 528 | public function unZip($zip_filename, $unzip_path) { 529 | mkdir($unzip_path, 0777, true); 530 | $this->setDirectory($unzip_path); 531 | $zip = new ZipArchive(); 532 | if ($zip->open($zip_filename)) { 533 | if (!$zip->extractTo($unzip_path)) { 534 | throw new Error(sprintf('Unable to extract file : %s', $zip_filename)); 535 | } 536 | return $zip->close(); 537 | } else { 538 | throw new Error(sprintf('Unable to open file : %s', $zip_filename)); 539 | } 540 | } 541 | 542 | /** 543 | * Set all array properties to empty arrays. 544 | * @return $this 545 | */ 546 | public function unsetArrays() { 547 | $this->setSpreads([]) 548 | ->setMasterSpreads([]) 549 | ->setStories([]); 550 | return $this; 551 | } 552 | 553 | /** 554 | * Special method to load the designmap.xml of this IDML which is required 555 | * to get all of the other components. 556 | * @return $this 557 | */ 558 | public function loadDesignMap() { 559 | $designmap = $this->createDom($this->getDirectory() . "designmap.xml"); 560 | $this->setDesignMap($designmap); 561 | return $this; 562 | } 563 | 564 | /** 565 | * This is the master load method that will create populate the object with 566 | * DOMDocuments of the component XML files. You need to set the location of 567 | * the IDML before this method is called - either by passing it with the 568 | * instantiation (new IDML\Package("idmlfile.idml")) or by 569 | * calling the setZip() or setDirectory() methods. 570 | * @return $this 571 | */ 572 | public function load() { 573 | if (!$this->getDirectory()) { 574 | $this->unZip($this->getZip(), $this->getUnZipPath()); 575 | } 576 | 577 | // since some files are appended to arrays, let's unset those arrays when we load just to be safe 578 | $this->unsetArrays(); 579 | 580 | $this->loadDesignMap(); 581 | 582 | $xpath = new DOMXPath($this->getDesignMap()); 583 | $xpath->registerNamespace(self::IDML_NAMESPACE_PREFIX, self::IDML_NAMESPACE_URI); 584 | // I just didn't want to type all the loading logic out so I did a complicated loop 585 | 586 | foreach ($this->packageElements as $packageElement => $setPrefix) { 587 | $elements = $xpath->query("//".self::IDML_NAMESPACE_PREFIX.":".$packageElement); 588 | 589 | if ($elements->length) { 590 | /** @var DOMElement $element */ 591 | foreach ($elements as $element) { 592 | $filename = $element->getAttribute("src"); 593 | $filepath = $this->getDirectory() . $filename; 594 | if (file_exists($filepath)) { 595 | $file = $this->createDom($filepath); 596 | $setter = $setPrefix.$packageElement; 597 | $this->$setter($file); 598 | } 599 | } 600 | } 601 | } 602 | 603 | return $this; 604 | } 605 | 606 | /** 607 | * Save the designmap.xml file to disk. 608 | * @return $this 609 | */ 610 | public function saveDesignMap() { 611 | $this->getDesignMap()->save($this->getDesignMap()->documentURI); 612 | return $this; 613 | } 614 | 615 | /** 616 | * Save the stories files to disk. 617 | * @return $this 618 | */ 619 | public function saveStories() { 620 | $this->saveArrayOfDoms($this->getStories()); 621 | return $this; 622 | } 623 | 624 | /** 625 | * Save the master spreads files to disk. 626 | * @return $this 627 | */ 628 | public function saveMasterSpreads() { 629 | $this->saveArrayOfDoms($this->getMasterSpreads()); 630 | return $this; 631 | } 632 | 633 | /** 634 | * Save the spreads files to disk. 635 | * @return $this 636 | */ 637 | public function saveSpreads() { 638 | $this->saveArrayOfDoms($this->getSpreads()); 639 | return $this; 640 | } 641 | 642 | /** 643 | * Saves all of the individual documents in the IDML package to their documentURI locations. 644 | * @param null|string $zip_file_path 645 | * @return $this 646 | */ 647 | public function saveAll($zip_file_path = null) { 648 | $this->saveDesignMap() 649 | ->saveStories() 650 | ->saveMasterSpreads() 651 | ->saveSpreads(); 652 | 653 | foreach ($this->packageElements as $element => $setPrefix) { 654 | if ($setPrefix == "set") { 655 | $getter = "get".$element; 656 | $file = $this->$getter(); 657 | 658 | if ($file instanceof DOMDocument) { 659 | $file->save($file->documentURI); 660 | } 661 | } 662 | } 663 | 664 | $zip_file_path = $zip_file_path ? $zip_file_path : ($this->isZip() ? $this->getZip() : null); 665 | if ($zip_file_path) { 666 | $this->zipPackage($zip_file_path); 667 | } 668 | 669 | return $this; 670 | } 671 | 672 | /** 673 | * Zip this package into an IDML file from its component parts. 674 | * @param string|null $zip_file_path [optional] If supplied, this is the filename of the zipped package. If not supplied, 675 | * this defaults to the name of the directory of this IDML package with a ".idml" extension. 676 | * @return $this 677 | */ 678 | public function zipPackage($zip_file_path = null) { 679 | $currentDirectory = getcwd(); 680 | chdir($this->getDirectory()); 681 | 682 | if (!$zip_file_path) { 683 | if ($this->isZip()) { 684 | $zip_file_path = basename($this->getZip()); 685 | } else { 686 | $zip_file_path = basename($this->getDirectory()) . self::IDML_FILENAME_EXTENSION; 687 | } 688 | } 689 | 690 | $zip = new ZipArchive(); 691 | $zip->open($zip_file_path, ZipArchive::CREATE); 692 | $dir = new SplFileInfo("."); 693 | 694 | // this is included here to make this class standalone, but ideally 695 | // you'd factor it out into a class that extends ZipArchive 696 | $contents = []; 697 | $baseDir = null; 698 | if ($dir->isDir()) { 699 | $contents = $this->getDirectoryContents($this->getDirectory()); 700 | $baseDir = $dir->getPathInfo()->getRealPath(); 701 | } 702 | 703 | if (count($contents) === 0) { 704 | $zip->addEmptyDir($dir->getBasename()); 705 | } else { 706 | foreach ($contents as $c) { 707 | if (is_dir($c)) { 708 | // safe to do because directories will always come before their contents 709 | // in the array returned by getDirectoryContents() 710 | $zip->addEmptyDir($dir->getBasename()); 711 | } else { 712 | $zip->addFile($c, str_replace($baseDir . DIRECTORY_SEPARATOR, "", $c)); 713 | } 714 | } 715 | } 716 | 717 | $zip->close(); 718 | $this->setZip($zip_file_path); 719 | chdir($currentDirectory); 720 | return $this; 721 | } 722 | 723 | /** 724 | * Adds a story to the story array of this IDML package. 725 | * @param DOMDocument $val The story to add. 726 | * @return $this 727 | */ 728 | public function addStory(DOMDocument $val) { 729 | $key = str_replace(["Story_", ".xml"], "", basename($val->documentURI)); 730 | $this->stories[$key] = $val; 731 | return $this; 732 | } 733 | 734 | /** 735 | * Adds a story to the designmap of the IDML package so InDesign knows it is there. 736 | * @param DOMDocument $val The story to add to the designmap. 737 | * @return $this 738 | */ 739 | public function addStoryToDesignMap(DOMDocument $val) { 740 | $this->addStory($val); 741 | $node = $this->getDesignMap()->createElement(self::IDML_NAMESPACE_PREFIX.":Story"); 742 | $this->getDesignMap()->documentElement->appendChild($node); 743 | $source = str_replace($this->getDirectory(), "", $val->documentURI); 744 | $node->setAttribute("src", $source); 745 | return $this; 746 | } 747 | 748 | /** 749 | * Add a spread to the spreads property of this package. 750 | * Does NOT do anything other than this - no file creation/saving/etc. 751 | * @param DOMDocument $val 752 | * @return $this 753 | */ 754 | public function addSpread(DOMDocument $val) { 755 | $this->spreads[$this->getSelfAttributeOfDom($val)] = $val; 756 | return $this; 757 | } 758 | 759 | /** 760 | * Adds an element to the spread of this IDML package. 761 | * See the notes on the getFirstSpread() method - you'll have to adjust this if your IDML 762 | * files have more than one spread. 763 | * @param DOMNode $val The element to be added to the spread. 764 | * @param DOMDocument $spread The spread you want to which you want to add the element. If not provided, defaults 765 | * to whatever getFirstSpread() returns. 766 | * @return $this 767 | */ 768 | public function addElementToSpread(DOMNode $val, DOMDocument $spread = null) { 769 | if (!$spread) { 770 | $spread = $this->getFirstSpread(); 771 | } 772 | 773 | $spreadNodelist = $spread->getElementsByTagName("Spread"); 774 | $spreadElement = $spreadNodelist->item($spreadNodelist->length - 1); 775 | $spreadElement->appendChild($spread->importNode($val, true)); 776 | return $this; 777 | } 778 | 779 | /** 780 | * Add a master spread to the master spreads property of this package. 781 | * Does NOT do anything other than this - no file creation/saving/etc. 782 | * @param DOMDocument $val 783 | * @return $this 784 | */ 785 | public function addMasterSpread(DOMDocument $val) { 786 | $this->masterSpreads[$this->getSelfAttributeOfDom($val)] = $val; 787 | return $this; 788 | } 789 | 790 | /** 791 | * This is a convenience method for the project that spawned this class. All of the IDML 792 | * files used in that project have only one spread so this was easier than typing it out 793 | * in a thousand different places. Don't use if your IDML files have multiple spreads. 794 | * @return DOMDocument The spread. 795 | */ 796 | public function getFirstSpread() { 797 | return array_values($this->getSpreads())[0]; 798 | } 799 | 800 | /** 801 | * Get layers from the designmap.xml of this IDML package. 802 | * @param bool $selfsOnly If true, returns only the self attributes of the layers. If false, 803 | * returns the layer elements. 804 | * @param bool $visibleOnly If true, returns only visible layers. If false, returns all layers. 805 | * @return string[]|DOMNodeList If $selfsOnly is true, this is an array of strings. Otherwise, it is a 806 | * DOMNodeList of the layer elements. 807 | */ 808 | public function getLayers($selfsOnly = false, $visibleOnly = true) { 809 | $xpath = new DOMXPath($this->getDesignMap()); 810 | $query = ($visibleOnly) ? "//Layer[@Visible='true']" : "//Layer"; 811 | $layers = $xpath->query($query); 812 | 813 | if ($selfsOnly) { 814 | $names = array_map( 815 | function (DOMElement $layer) { 816 | return $layer->getAttribute("Self"); 817 | }, 818 | (array) $layers 819 | ); 820 | 821 | sort($names); 822 | return array_unique($names); 823 | } 824 | 825 | return $layers; 826 | } 827 | 828 | /** 829 | * Returns the specified DOM element if it can be found within the package. 830 | * @param string $self The self attibute of the requested DOM element. Generally something like "u12f". 831 | * @throws Error if the specified node cannot be found. 832 | * @return DOMNode A DOMNode of the requested element if it is found. 833 | */ 834 | public function getElementBySelfAttribute($self = "") { 835 | if ($self !== "") { 836 | $doms = array_merge( 837 | $this->getSpreads(), 838 | $this->getMasterSpreads(), 839 | $this->getStories(), 840 | [$this->getBackingStory()] 841 | ); 842 | 843 | foreach ($doms as $dom) { 844 | $xpath = new DOMXPath($dom); 845 | $elements = $xpath->query("//node()[@Self='".$self."']"); 846 | 847 | if ($elements->length > 0) { 848 | return $elements->item(0); 849 | } 850 | } 851 | } 852 | 853 | throw new Error(sprintf("Unable to find DOM node with self attribute %s.", $self)); 854 | } 855 | 856 | /** 857 | * Determine whether this IDML package was loaded from a zipped .idml package, or from an unzipped directory. 858 | * @return bool True means it was created from a zip. False means it was created from a directory. 859 | */ 860 | protected function isZip() { 861 | return ($this->getZip() && basename($this->getDirectory())[0] === "."); 862 | } 863 | 864 | /** 865 | * Create a DOMDocument and load it with the supplied file. 866 | * @param string $filename The name of the file to be loaded. 867 | * @return DOMDocument The object that was created. 868 | * @throws Error 869 | */ 870 | protected function createDom($filename) { 871 | $dom = new DOMDocument(); 872 | $dom->preserveWhiteSpace = false; 873 | $dom->formatOutput = true; 874 | 875 | if ($filename && file_exists($filename)) { 876 | $dom->load($filename); 877 | return $dom; 878 | } else { 879 | throw new Error(sprintf("Unable to load file : %s.", $filename)); 880 | } 881 | } 882 | 883 | /** 884 | * Saves each DOMDocument in an array to disk, using its documentURI property as the file location. 885 | * @param array $array An array of DOMDocuments. 886 | * @return $this 887 | */ 888 | protected function saveArrayOfDoms(array $array) { 889 | foreach ($array as $dom) { 890 | $dom->save($dom->documentURI); 891 | } 892 | 893 | return $this; 894 | } 895 | 896 | /** 897 | * A convenience method to get the self attribute of the main element of a component. 898 | * e.g. for a story, this returns u123 from 899 | * @param DOMDocument $dom 900 | * @return string 901 | */ 902 | protected function getSelfAttributeOfDom(DOMDocument $dom) { 903 | $elementName = str_replace(self::IDML_NAMESPACE_PREFIX.":", "", $dom->documentElement->nodeName); 904 | $element = $dom->documentElement->getElementsByTagName($elementName)->item(0); 905 | return $element->getAttribute("Self"); 906 | } 907 | 908 | /** 909 | * Returns the ParagraphStyle or CharacterStyle node from the Styles.xml file that is 910 | * assocated with the given $node. 911 | * @param DOMElement $node The node whose AppliedStyle you want. 912 | * @throws Error if the style node you want could not be found. 913 | * @return DOMElement Either the DOMNode of the applied style you want. 914 | */ 915 | public function getAppliedStyle(DOMElement $node) { 916 | $xpath = new DOMXPath($this->getStyles()); 917 | $nodeType = str_replace("StyleRange", "", $node->nodeName); 918 | $type = "Applied{$nodeType}Style"; 919 | $style = $node->getAttribute($type); 920 | 921 | if (!$style) { 922 | throw new Error("Unable to find style node for given {$node->nodeName}."); 923 | } 924 | 925 | $nodeList = $xpath->query("//node()[@Self='".$style."']"); 926 | 927 | if ($nodeList->length > 0) { 928 | return $nodeList->item(0); 929 | } 930 | 931 | throw new Error(sprintf("Unable to find style node for given {%s}.", $node->nodeName)); 932 | } 933 | 934 | /** 935 | * Searches for a given style attribute as exhaustively as possible. 936 | * @param DOMElement $node The node whose attribute you want. 937 | * @param string $attr The name of the attribute you want. 938 | * @throws Error if attribute could not be found. 939 | * @return string Either a string of the attribute you want. 940 | */ 941 | public function getStyleAttribute(DOMElement $node, $attr) { 942 | $parent = $node->parentNode; 943 | $appliedCSR = $this->getAppliedStyle($node); 944 | $appliedPSR = $this->getAppliedStyle($parent); 945 | 946 | // order of elements here is intentional 947 | foreach ([$node, $appliedCSR, $parent, $appliedPSR] as $element) { 948 | $ret = $element->getAttribute($attr); 949 | if ($ret) { 950 | return $ret; 951 | } 952 | } 953 | 954 | throw new Error(sprintf("Unable to find value for attribute %s.", $attr)); 955 | } 956 | 957 | /** 958 | * Searches for a given style property as exhaustively as possible. 959 | * @param DOMElement $node The node whose property you want. 960 | * @param string $prop The name of the property you want. 961 | * @throws Error if property could not be found. 962 | * @return string Either a string of the property you want. 963 | */ 964 | public function getStyleProperty(DOMElement $node, $prop) { 965 | $parent = $node->parentNode; 966 | $appliedCSR = $this->getAppliedStyle($node); 967 | $appliedPSR = $this->getAppliedStyle($parent); 968 | $propertyGroups = []; 969 | 970 | // order of elements here is intentional 971 | /** @var DOMElement $element */ 972 | foreach ([$node, $appliedCSR, $parent, $appliedPSR] as $element) { 973 | $p = $element->getElementsByTagName("Properties"); 974 | if ($p->length > 0) { 975 | $propertyGroups[] = $p->item(0); 976 | } 977 | } 978 | 979 | /** @var DOMElement $group */ 980 | foreach ($propertyGroups as $group) { 981 | $propList = $group->getElementsByTagName($prop); 982 | if ($propList->length > 0) { 983 | return $propList->item(0)->nodeValue; 984 | } 985 | } 986 | 987 | throw new Error(sprintf("Unable to find value for property %s.", $prop)); 988 | } 989 | 990 | /** 991 | * Get the tag of an element from the XML/BackingStory.xml file. 992 | * @param DOMElement $node The element for which we want the tag. 993 | * @throws Error if the tag could not be found. 994 | * @return string The tag if one is found. 995 | */ 996 | public function getMarkupTag(DOMElement $node) { 997 | $tag = false; 998 | $selfs = [$node->getAttribute("Self")]; 999 | $xpath = new DOMXPath($this->getBackingStory()); 1000 | 1001 | if ($node->nodeName === "TextFrame") { 1002 | $selfs[] = $node->getAttribute("ParentStory"); 1003 | } 1004 | 1005 | foreach ($selfs as $self) { 1006 | $xmlElement = false; 1007 | 1008 | while ($node->parentNode) { 1009 | /** @var DOMElement $node */ 1010 | $node = $node->parentNode; 1011 | if ("XMLElement" === $node->nodeName) { 1012 | $xmlElement = $node; 1013 | break; 1014 | } 1015 | } 1016 | 1017 | if ($xmlElement === false) { 1018 | $xmlElements = $xpath->query("//XMLElement[@XMLContent='".$self."']"); 1019 | if ($xmlElements->length > 0) { 1020 | $xmlElement = $xmlElements->item(0); 1021 | } 1022 | } 1023 | 1024 | if ($xmlElement && $xmlElement->getAttribute("MarkupTag")) { 1025 | $tag = str_replace("XMLTag/", "", $xmlElement->getAttribute("MarkupTag")); 1026 | } 1027 | } 1028 | 1029 | if ($tag !== false) { 1030 | return urldecode($tag); 1031 | } 1032 | 1033 | throw new Error(sprintf("Unable to find markup tag for given {%s} node.", $node->nodeName)); 1034 | } 1035 | } 1036 | --------------------------------------------------------------------------------