diff --git a/src/Data.php b/src/Data.php new file mode 100644 index 0000000000000000000000000000000000000000..8e8c6afa02ebe6220e91586e135ec0436c6d3739 --- /dev/null +++ b/src/Data.php @@ -0,0 +1,61 @@ +<?php + +namespace Unicaen\OpenDocument; + +use Exception; +use DOMDocument; +use ZipArchive; + +/** + * Class Data + * @todo à terminer : non fonctionnel + * @package Unicaen\OpenDocument + */ +class Data +{ + const PERIMETRE_TAB_LIGNE = 'table:table-row'; + const PERIMETRE_FRAME = 'draw:frame'; + const PERIMETRE_LIST_ITEM = 'text:list-item'; + + /** + * @var string[] + */ + public $variables = []; + + /** + * @var + */ + public $subDataVariable; + + /** + * @var + */ + public $subDataPerimetre; + + /** + * @var Data[] + */ + public $subData = []; + + + + public static function create(array $variables = []) + { + $data = new self; + $data->variables = $variables; + + return $data; + } + + + + public function addSubData(string $variable, string $perimetre, array $variables = []) + { + $subData = new self; + $subData->subDataVariable = $variable; + $subData->subDataPerimetre = $perimetre; + $subData->variables = $variables; + + return $subData; + } +} \ No newline at end of file diff --git a/src/Document.php b/src/Document.php new file mode 100644 index 0000000000000000000000000000000000000000..14ab3e24f05d0613e7ab96a7d4dfee2e4780da60 --- /dev/null +++ b/src/Document.php @@ -0,0 +1,822 @@ +<?php + +namespace Unicaen\OpenDocument; + +use DOMDocument; +use DOMElement; +use DOMNode; +use DOMNodeList; +use Exception; +use ZipArchive; + + +class Document +{ + + /** + * @var ZipArchive + */ + private $archive; + + /** + * @var bool + */ + private $pdfOutput = false; + + /** + * @var Publisher + */ + private $publisher; + + /** + * @var Stylist + */ + private $stylist; + + /** + * @var DomDocument; + */ + private $meta; + + /** + * @var DOMDocument + */ + private $styles; + + /** + * @var DOMDocument + */ + private $content; + + /** + * @var array + */ + private $namespaces = []; + + /** + * @var bool + */ + private $metaChanged = false; + + /** + * @var bool + */ + private $stylesChanged = false; + + /** + * @var bool + */ + private $contentChanged = false; + + /** + * Sous Debian, vous devrez autoriser www-data à utiliser unoconv avec sudo : + * + * $ visudo + * + * puis ajouter ceci: + * www-data ALL=(ALL) NOPASSWD: /usr/bin/unoconv + * + * @var string + */ + private $convCommand = 'sudo unoconv -f pdf -o :outputFile :inputFile'; + + /** + * @var string + */ + private $tmpDir; + + /** + * @var array + */ + private $tmpFiles = []; + + private $defaultNamespaces = [ + 'office' => "urn:oasis:names:tc:opendocument:xmlns:office:1.0", + 'style' => "urn:oasis:names:tc:opendocument:xmlns:style:1.0", + 'text' => "urn:oasis:names:tc:opendocument:xmlns:text:1.0", + 'table' => "urn:oasis:names:tc:opendocument:xmlns:table:1.0", + 'draw' => "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", + 'fo' => "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", + 'xlink' => "http://www.w3.org/1999/xlink", + 'dc' => "http://purl.org/dc/elements/1.1/", + 'meta' => "urn:oasis:names:tc:opendocument:xmlns:meta:1.0", + 'number' => "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0", + 'svg' => "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0", + 'chart' => "urn:oasis:names:tc:opendocument:xmlns:chart:1.0", + 'dr3d' => "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0", + 'math' => "http://www.w3.org/1998/Math/MathML", + 'form' => "urn:oasis:names:tc:opendocument:xmlns:form:1.0", + 'script' => "urn:oasis:names:tc:opendocument:xmlns:script:1.0", + 'ooo' => "http://openoffice.org/2004/office", + 'ooow' => "http://openoffice.org/2004/writer", + 'oooc' => "http://openoffice.org/2004/calc", + 'dom' => "http://www.w3.org/2001/xml-events", + 'rpt' => "http://openoffice.org/2005/report", + 'of' => "urn:oasis:names:tc:opendocument:xmlns:of:1.2", + 'xhtml' => "http://www.w3.org/1999/xhtml", + 'grddl' => "http://www.w3.org/2003/g/data-view#", + 'officeooo' => "http://openoffice.org/2009/office", + 'tableooo' => "http://openoffice.org/2009/table", + 'drawooo' => "http://openoffice.org/2010/draw", + 'calcext' => "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0", + 'css3t' => "http://www.w3.org/TR/css3-text/", + 'xforms' => 'http://www.w3.org/2002/xforms', + 'xsd' => 'http://www.w3.org/2001/XMLSchema', + 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + 'loext' => 'urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0', + 'field' => 'urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0', + 'formx' => 'urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0', + ]; + + + + /** + * @return ZipArchive + * @throws Exception + */ + public function getArchive(): ZipArchive + { + if (!$this->archive) { + throw new \Exception("Aucun document n\'est chargé"); + } + + return $this->archive; + } + + + + /** + * @return DOMDocument + */ + public function getContent(): DOMDocument + { + return $this->content; + } + + + + /** + * @return DOMDocument + */ + public function getStyles(): DOMDocument + { + return $this->styles; + } + + + + /** + * @return DOMDocument + */ + public function getMeta(): DOMDocument + { + return $this->meta; + } + + + + /** + * @return bool + */ + public function isPdfOutput(): bool + { + return $this->pdfOutput; + } + + + + /** + * @param bool $pdfOutput + * + * @return Document + */ + public function setPdfOutput(bool $pdfOutput): Document + { + $this->pdfOutput = $pdfOutput; + + return $this; + } + + + + /** + * @return bool + */ + public function isMetaChanged(): bool + { + return $this->metaChanged; + } + + + + /** + * @param bool $metaChanged + * + * @return Document + */ + public function setMetaChanged(bool $metaChanged): Document + { + $this->metaChanged = $metaChanged; + + return $this; + } + + + + /** + * @return bool + */ + public function isStylesChanged(): bool + { + return $this->stylesChanged; + } + + + + /** + * @param bool $stylesChanged + * + * @return Document + */ + public function setStylesChanged(bool $stylesChanged): Document + { + $this->stylesChanged = $stylesChanged; + + return $this; + } + + + + /** + * @return bool + */ + public function isContentChanged(): bool + { + return $this->contentChanged; + } + + + + /** + * @param bool $contentChanged + * + * @return Document + */ + public function setContentChanged(bool $contentChanged): Document + { + $this->contentChanged = $contentChanged; + + return $this; + } + + + + /** + * @return string + */ + public function getConvCommand(): string + { + return $this->convCommand; + } + + + + /** + * @param string $convCommand + * + * @return Document + */ + public function setConvCommand(string $convCommand): Document + { + $this->convCommand = $convCommand; + + return $this; + } + + + + /** + * Retourne les méta-données du fichier OpenDocument + * + * @return array + */ + public function getMetaArray(): array + { + $m = []; + + $nodes = $this->getMeta()->documentElement->childNodes->item(0)->childNodes; + foreach ($nodes as $node) { + if (isset($node->tagName)) { + switch ($node->tagName) { + case 'meta:generator': + case 'meta:initial-creator': + case 'meta:editing-cycles': + case 'meta:editing-duration': + case 'meta:printed-by': + case 'dc:title': + case 'dc:description': + case 'dc:subject': + case 'dc:creator': + case 'dc:language': + list($ns, $tag) = explode(':', $node->tagName); + $m[$tag] = $node->textContent; + break; + case 'meta:creation-date': + $m['creation-date'] = substr($node->textContent, 0, 10); + break; + case 'meta:print_date': + $m['print_date'] = substr($node->textContent, 0, 10); + break; + case 'dc:date': + $m['date'] = substr($node->textContent, 0, 10); + break; + case 'meta:document-statistic': + $m['document-statistic'] = []; + foreach ($node->attributes as $attribute) { + $m['document-statistic'][$attribute->name] = $attribute->value; + } + break; + case 'meta:user-defined': + if (!isset($m['user-defined'])) $m['user-defined'] = []; + foreach ($node->attributes as $attribute) { + $m['user-defined'][$attribute->name] = $attribute->value; + } + break; + case 'meta:keywords': + $m['keywords'] = []; + foreach ($node->childNodes as $knode) { + $m['keywords'][] = $knode->textContent; + } + break; + } + } + } + + return $m; + } + + + + /** + * Retourne les espaces de nom associés à leurs URI respectives sous forme de tableau associatif + * + * @return array + */ + public function getNamespaces(): array + { + if (empty($this->namespaces)) { + $content = $this->getArchive()->getFromName('content.xml'); + + $begin = strpos($content, '<', 1) + 2 + strlen('office:document-content'); + $end = strpos($content, '>', 50) - $begin; + $content = explode(' ', substr($content, $begin, $end)); + $this->namespaces = $this->defaultNamespaces; + foreach ($content as $str) { + if (0 === strpos($str, 'xmlns:')) { + $namespace = substr($str, 6, strpos($str, '"') - 7); + $url = substr($str, strpos($str, '"') + 1, -1); + $this->namespaces[$namespace] = $url; + } + } + } + + return $this->namespaces; + } + + + + /** + * @param string $name + * + * @return bool + */ + public function hasNamespace(string $name): bool + { + $this->getNamespaces(); + + return isset($this->namespaces[$name]); + } + + + + /** + * @param string $name + * @param string $url + * + * @return Document + */ + public function addNamespace(string $name, string $url): Document + { + if (!$this->hasNamespace($name)) { + $this->namespaces[$name] = $url; + } + + return $this; + } + + + + /** + * Retourne l'url associé à un espace de nom + * + * @param string $namespace + * + * @return string + */ + public function getNamespaceUrl(string $namespace): string + { + $ns = $this->getNamespaces(); + + if (!isset($ns[$namespace])) { + throw new Exception('L\'espace de nom ' . $namespace . ' n\'a pas été trouvé.'); + } + + return $ns[$namespace]; + } + + + + /** + * Retourne un champ d'information + * + * @param integer $index + * + * @return string + */ + public function getInfo($index) + { + $infonodes = $this->getMeta()->getElementsByTagNameNS($this->getNamespaceUrl('meta'), 'user-defined'); + + return $infonodes->item($index)->nodeValue; + } + + + + /** + * Modifie un champ d'information + * + * @todo à finir d'implémenter, car ça ne marche pas!! + * + * @param integer $index + * @param string $value + */ + public function setInfo($index, $value): Document + { + throw new \Exception('Implémentation à corriger : ne fonctionne pas'); + + $infonodes = $this->getMeta()->getElementsByTagNameNS($this->getNamespaceUrl('meta'), 'user-defined'); + if ($infonodes->length > 0) { + $infonodes->item($index)->nodeValue = $value; + $this->setMetaChanged(true); + } + + return $this; + } + + + + /** + * @param array $values + * + * @return Document + */ + public function publish(array $values): Document + { + $this->getPublisher()->setValues($values); + $this->getPublisher()->publish(); + + return $this; + } + + + + /** + * @return Publisher + */ + public function getPublisher(): Publisher + { + if (!$this->publisher) { + $this->publisher = new Publisher(); + $this->publisher->setDocument($this); + } + + return $this->publisher; + } + + + + /** + * @return Stylist + */ + public function getStylist(): Stylist + { + if (!$this->stylist) { + $this->stylist = new Stylist(); + $this->stylist->setDocument($this); + } + + return $this->stylist; + } + + + + /** + * @param $data + * + * @return Document + * @throws Exception + */ + public function loadFromData($data): Document + { + if (!class_exists('ZipArchive')) { + throw new Exception('Zip extension not loaded'); + } + + $odtFile = $this->tempFileName('odtfile_', 'odt'); + file_put_contents($odtFile, $data); + + return $this->loadFromFile($odtFile, false); + } + + + + /** + * @param string $fileName + * @param bool $duplicate + * + * @return Document + * @throws Exception + */ + public function loadFromFile(string $fileName, bool $duplicate = true): Document + { + if (!class_exists('ZipArchive')) { + throw new Exception('Zip extension not loaded'); + } + + if (!file_exists($fileName)) { + throw new Exception('OpenDocument file "' . $fileName . '" doesn\'t exists.'); + } + + if ($duplicate) { + $odtFile = $this->tempFileName('odtFile_', 'odt'); + copy($fileName, $odtFile); + } else { + $odtFile = $fileName; + } + + $this->archive = new ZipArchive(); + if (!true === $this->archive->open($odtFile, ZIPARCHIVE::CREATE)) { + throw new Exception('OpenDocument file "' . $fileName . '" don\'t readable.'); + } + + $this->meta = new DOMDocument; + $this->meta->loadXML($this->archive->getFromName('meta.xml')); + + $this->styles = new DOMDocument; + $this->styles->loadXML($this->archive->getFromName('styles.xml')); + + $this->content = new DOMDocument; + $this->content->loadXML($this->archive->getFromName('content.xml')); + + $this->namespaces = []; + + return $this; + } + + + + /** + * @return string + * @throws Exception + */ + private function prepareSaving($filename = null): string + { + if ($this->isMetaChanged()) { + $this->getArchive()->addFromString('meta.xml', $this->getMeta()->saveXML()); + } + + if ($this->isStylesChanged()) { + $this->getArchive()->addFromString('styles.xml', $this->getStyles()->saveXML()); + } + + if ($this->isContentChanged()) { + $this->getArchive()->addFromString('content.xml', $this->getContent()->saveXML()); + } + + $actualFile = $this->getArchive()->filename; + $this->getArchive()->close(); + $this->archive = null; + + if ($this->isPdfOutput()) { + if (!$filename) { + $filename = $this->tempFileName('odt2pdf_', 'pdf'); + } + + $this->odtToPdf($actualFile, $filename); + $actualFile = $filename; + } + + return $actualFile; + } + + + + /** + * @param $origine + * @param $destination + * + * @return Document + * @throws Exception + */ + public function odtToPdf($origine, $destination): Document + { + $command = $this->getConvCommand(); + + $command = str_replace(':inputFile', $origine, $this->getConvCommand()); + $command = str_replace(':outputFile', $destination, $command); + + exec($command, $output, $return); +// sleep(10); + if (0 != $return) { + throw new \Exception('La conversion de document en PDF a échoué'); + } + + return $this; + } + + + + /** + * @param string $fileName + * + * @return Document + * @throws Exception + */ + public function saveToFile(string $fileName): Document + { + $actualFile = $this->prepareSaving($fileName); + + return $this; + } + + + + /** + * @return string + * @throws Exception + */ + public function saveToData(): string + { + $actualFile = $this->prepareSaving(); + + return file_get_contents($actualFile); + } + + + + /** + * @return string + */ + public function getTmpDir(): string + { + if (!$this->tmpDir) { + return sys_get_temp_dir(); + } + + return $this->tmpDir; + } + + + + /** + * @param string $tmpDir + * + * @return Document + */ + public function setTmpDir(string $tmpDir): Document + { + $this->tmpDir = $tmpDir; + if (!file_exists($tmpDir)) { + mkdir($tmpDir); + } + + return $this; + } + + + + /** + * @param null $prefix + * + * @return string + */ + private function tempFileName($prefix = null, $ext = 'odt'): string + { + $tmpDir = $this->getTmpDir(); + if ('/' != substr($tmpDir, -1)) { + $tmpDir .= '/'; + } + + $tempFileName = uniqid($tmpDir . $prefix) . '.' . $ext; + $this->tmpFiles[] = $tempFileName; + + return $tempFileName; + } + + + + /** + * PHP 5 introduces a destructor concept similar to that of other object-oriented languages, such as C++. + * The destructor method will be called as soon as all references to a particular object are removed or + * when the object is explicitly destroyed or in any order in shutdown sequence. + * + * Like constructors, parent destructors will not be called implicitly by the engine. + * In order to run a parent destructor, one would have to explicitly call parent::__destruct() in the destructor body. + * + * Note: Destructors called during the script shutdown have HTTP headers already sent. + * The working directory in the script shutdown phase can be different with some SAPIs (e.g. Apache). + * + * Note: Attempting to throw an exception from a destructor (called in the time of script termination) causes a fatal + * error. + * + * @return void + * @link https://php.net/manual/en/language.oop5.decon.php + */ + public function __destruct() + { + /* On supprime les éventuels fichiers temporaires pour libérer l'espace */ + foreach ($this->tmpFiles as $tmpFile) { + if (file_exists($tmpFile)) { + unlink($tmpFile); + } + } + } + + + + /** + * @param $fileName + * + * @return Document + * @throws Exception + */ + public function download($fileName): Document + { + $actualFile = $this->prepareSaving(); + + if (headers_sent()) { + throw new Exception('Headers Allready Sent'); + } + + $contentType = $this->isPdfOutput() ? 'application/pdf' : 'application/vnd.oasis.opendocument.text'; + + header('Content-type: ' . $contentType); + header('Content-Disposition: attachment; filename="' . $fileName . '"'); + header('Content-Transfer-Encoding: binary'); + header('Pragma: no-cache'); + header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); + header('Expires: 0'); + readfile($actualFile); + + return $this; + } + + + + /** + * @param DOMNode $node + * @param string $name + * + * @return DOMNode[] + * @throws Exception + */ + public function find(DOMNode $node, string $name): DOMNodeList + { + list($namespace, $name) = explode(':', $name); + + return $node->getElementsByTagNameNS($this->getNamespaceUrl($namespace), $name); + } + + + + /** + * @param DOMDocument $document + * @param string $name + * @param array $attrs + * + * @return DOMElement + * @throws Exception + */ + public function newElement(DOMDocument $document, string $name, array $attrs = []): DOMElement + { + list($namespace) = explode(':', $name); + + $newNode = $document->createElementNS($this->getNamespaceUrl($namespace), $name); + foreach ($attrs as $attrName => $attrValue) { + list($attrNS) = explode(':', $attrName); + $newNode->setAttributeNS($this->getNamespaceUrl($attrNS), $attrName, $attrValue); + } + + return $newNode; + } +} diff --git a/src/Publisher.php b/src/Publisher.php new file mode 100644 index 0000000000000000000000000000000000000000..ab6e8a410a82009ac26c5f217cb9b13b016c1455 --- /dev/null +++ b/src/Publisher.php @@ -0,0 +1,433 @@ +<?php + +namespace Unicaen\OpenDocument; + +use Exception; +use DOMDocument; +use DOMElement; +use ZipArchive; + +class Publisher +{ + const PAGE_BREAK_NAME = 'UNICAEN_PAGE_BREAK'; + + /** + * Lecteur de fichier OpenDocument + * + * @var Document + */ + private $document; + + /** + * Contenu XML du corps de texte + * + * @var DOMDocument + */ + private $content; + + /** + * @var DOMElement + */ + private $body; + + /** + * Ajoute un saut de page automatiquement entre deux instances de document lors du publipostage + * + * @var boolean + */ + private $autoBreak = true; + + /** + * @var array + */ + private $values = []; + + /** + * Variable contenant le résultat final du content.xml + * + * @var string + */ + private $outContent; + + + + /** + * @return Document + */ + public function getDocument(): Document + { + return $this->document; + } + + + + /** + * @param Document $document + * + * @return Publisher + */ + public function setDocument(Document $document): Publisher + { + $this->document = $document; + + return $this; + } + + + + /** + * Peuple l'instance courante du document et l'ajoute à la suite du fichier + * + * @param array $values + */ + public function add(array $values): Publisher + { + $this->values[] = $values; + + return $this; + } + + + + /** + * @return array + */ + public function getValues(): array + { + return $this->values; + } + + + + /** + * @param array $values + */ + public function setValues(array $values): Publisher + { + $this->values = $values; + + return $this; + } + + + + /** + * @return bool + */ + public function isAutoBreak(): bool + { + return $this->autoBreak; + } + + + + /** + * @param bool $autoBreak + * + * @return Publisher + */ + public function setAutoBreak(bool $autoBreak): Publisher + { + $this->autoBreak = $autoBreak; + + return $this; + } + + + + /** + * Crée un sous-document à parcourir ensuite + * + * @param string $node_name + * @param string $variable_name + * + * @return Query + */ + public function subDoc($nodeName, $variableName) + { + return $this->query->subDoc($nodeName, $variableName); + } + + + + /** + * Cache une section + * + * @param string $sectionName + */ + public function hideSection($sectionName) + { + $this->query->hideSection($sectionName); + } + + + + /** + * @param DOMElement $element + * + * @return Publisher + * @throws Exception + */ + public function getVariables(DOMElement $element): array + { + $textNs = $this->getDocument()->getNamespaceUrl('text'); + + $variables = []; + $vElements = $element->getElementsByTagNameNS($textNs, 'variable-set'); + foreach ($vElements as $vElement) { + $name = $vElement->getAttributeNS($textNs, 'name'); + + if (!isset($variables[$name])) $variables[$name] = []; + $variables[$name][] = $vElement; + } + + return $variables; + } + + + + /** + * @param DOMElement $element + * @param string $value + * + * @return Publisher + */ + public function setVariable(DOMElement $element, string $value): Publisher + { + $textNs = $this->getDocument()->getNamespaceUrl('text'); + + $document = $element->ownerDocument; + + $value = explode("\n", $value); + for ($i = 0; $i < count($value); $i++) { + if ($i > 0) { + $returnVNode = $document->createElementNS($textNs, 'text:line-break'); + $element->parentNode->insertBefore($returnVNode, $element); + } + $vText = $document->createTextNode($value[$i]); + $element->parentNode->insertBefore($vText, $element); + } + $element->parentNode->removeChild($element); + + return $this; + } + + + + private function addPageBreakStyle(): Publisher + { + /* get office:automatic-styles node */ + $styles = $this->content->getElementsByTagNameNS( + $this->getDocument()->getNamespaceUrl('office'), + 'automatic-styles' + )->item(0); + + $styleNs = $this->getDocument()->getNamespaceUrl('style'); + + $stylePageBreak = $this->content->createElementNS($styleNs, 'style:style'); + $stylePageBreak->setAttributeNS($styleNs, 'style:name', self::PAGE_BREAK_NAME); + $stylePageBreak->setAttributeNS($styleNs, 'style:family', 'paragraph'); + $stylePageBreak->setAttributeNS($styleNs, 'style:parent-style-name', 'Standard'); + + $stylePageBreakProperties = $this->content->createElementNS($styleNs, 'style:paragraph-properties'); + $stylePageBreakProperties->setAttribute('fo:break-after', 'page'); + + $stylePageBreak->appendChild($stylePageBreakProperties); + $styles->appendChild($stylePageBreak); + + return $this; + } + + + + private function addPageBreak(DOMElement $element): Publisher + { + $textNs = $this->getDocument()->getNamespaceUrl('text'); + + $pageBreak = $this->content->createElementNS($textNs, 'text:p'); + $pageBreak->setAttributeNS($textNs, 'text:style-name', self::PAGE_BREAK_NAME); + $element->insertBefore($pageBreak, $element->firstChild); + + /*$pageBreak = "<text:p text:style-name=\"".self::PAGE_BREAK_NAME."\"></text:p>"; + $xml = str_replace('<office:text>','<office:text>'.$pageBreak, $xml);*/ + + return $this; + } + + + + /** + * @return Publisher + * @throws Exception + */ + private function publishBegin(): Publisher + { + $contentText = $this->content->saveXML(); + $officeDocumentContentPos = strpos($contentText, '<office:document-content'); + $length = strpos($contentText, '>', $officeDocumentContentPos) + 1; + $this->out(substr($contentText, 0, $length)); + + /* wtite all nodes, except body */ + foreach ($this->content->documentElement->childNodes as $node) { + if ($node->nodeName != 'office:body') { + $this->out($this->content->saveXML($node)); + } + } + + $this->out("<office:body>"); + /* declaration tags */ + $declTags = [ + 'variable-decls', 'sequence-decls', 'user-field-decls', 'dde-connexion-decls', + ]; + foreach ($declTags as $tagName) { + $node = $this->content->getElementsByTagNameNS($this->getDocument()->getNamespaceUrl('text'), $tagName); + if ($node->length > 0) { + $this->out($this->content->saveXML($node->item(0))); + $node->item(0)->parentNode->removeChild($node->item(0)); + } + } + + return $this; + } + + + + /** + * @return Publisher + */ + private function publishEnd(): Publisher + { + $this->out("</office:body></office:document-content>"); + + return $this; + } + + + + /** + * @param DOMElement $element + * @param array $values + * + * @return Publisher + * @throws Exception + */ + private function publishValues(DOMElement $element, array $values): Publisher + { + $variables = $this->getVariables($element); + + foreach ($values as $name => $val) { + if (is_array($val)) { + /* On traite les données filles... */ + list($vname, $vparent) = explode("@", $name); + if (isset($variables[$vname])) { + foreach ($variables[$vname] as $elVar) { + $this->publishSubData($elVar, $vparent, $val); + } + } + } elseif (isset($variables[$name])) { + foreach ($variables[$name] as $vElement) { + $this->setVariable($vElement, $val); + } + } + } + + return $this; + } + + + + /** + * @param DOMElement $element + * @param string $parent + * @param array $values + * + * @return Publisher + */ + private function publishSubData(DOMElement $element, string $parent, array $values): Publisher + { + $i = 10; + $found = false; + for ($i = 0; $i < 10; $i++) { + $parentNode = isset($parentNode) ? $parentNode->parentNode : $element->parentNode; + if ($parentNode->nodeName == $parent) { + $found = true; + break; + } + } + + if (!$found) { + throw new \Exception('Le noeud parent de type ' . $parent . ' n\'a pas été trouvé'); + } + + foreach( $values as $vals ){ + $clone = $parentNode->cloneNode(true); + $this->publishValues($clone, $vals); + + $parentNode->parentNode->insertBefore($clone, $parentNode); + } + + $parentNode->parentNode->removeChild($parentNode); + + return $this; + } + + + + /** + * Construit le fichier final à partir des données + */ + public function publish() + { + /* On récupère le content du document */ + $this->content = new DOMDocument(); + $this->content->loadXML($this->getDocument()->getContent()->saveXML()); + + if ($this->isAutoBreak()) { + $this->addPageBreakStyle(); + } + + $this->publishBegin(); + $this->body = $this->content->getElementsByTagNameNS($this->getDocument()->getNamespaceUrl('office'), 'text')[0]; + + $first = true; + foreach ($this->values as $values) { + $bodyNode = $this->body->cloneNode(true); + + if (!$first && $this->isAutoBreak()) { + $this->addPageBreak($bodyNode); + } + + $this->publishValues($bodyNode, $values); + $this->out($this->content->saveXML($bodyNode)); + $first = false; + } + + $this->publishEnd(); + + /* On renvoie le content dans le document */ + $this->getDocument()->getArchive()->addFromString('content.xml', $this->outContent); + } + + + + /** + * Ajoute du contenu au fichier content.xml + * Méthode à ne pas exploiter + * + * @param string $xml + */ + public function out($xml) + { + $this->outContent .= $xml; + } + + + + /** + * @return string + */ + public function getOutContent(): string + { + return $this->outContent; + } +} \ No newline at end of file diff --git a/src/Stylist.php b/src/Stylist.php new file mode 100644 index 0000000000000000000000000000000000000000..e12d9db49c6adefb55e814934d30836765f2899b --- /dev/null +++ b/src/Stylist.php @@ -0,0 +1,248 @@ +<?php + +namespace Unicaen\OpenDocument; + +use Exception; +use DOMDocument; +use DOMElement; +use DOMNode; +use DOMNodeList; +use UnicaenAuth\Service\PrivilegeServiceFactory; +use ZipArchive; + +class Stylist +{ + + /** + * Lecteur de fichier OpenDocument + * + * @var Document + */ + private $document; + + + + /** + * @return Document + */ + public function getDocument(): Document + { + return $this->document; + } + + + + /** + * @param Document $document + * + * @return Stylist + */ + public function setDocument(Document $document): Stylist + { + $this->document = $document; + + return $this; + } + + + + public function getAutomaticStyles() + { + $res = $this->find('office:automatic-styles'); + + return $res->item(0); + } + + + + /** + * @param $name + * @param $family + * @param array $properties + * + * @return DOMElement + */ + public function addAutomaticStyle($name, $family, array $properties = []): DOMElement + { + $ns = $this->newElement('style:style', [ + 'style:name' => $name, + 'style:family' => $family, + ]); + foreach ($properties as $proName => $proAttrs) { + $p = $this->newElement($proName, $proAttrs); + $ns->appendChild($p); + } + $this->getAutomaticStyles()->appendChild($ns); + + return $ns; + } + + + + public function addFiligrane($text, array $options = []): Stylist + { + $dw = strlen($text) * 2; + if ($dw > 20) $dw = 20; + + $dh = $dw / strlen($text) * 2; + + $defaultOptions = [ + 'color' => '#c0c0c0', + 'opacity' => 0.5, + 'font-name' => 'Liberation Sans', + 'width' => $dw, + 'height' => $dh, + ]; + + $options = array_merge($defaultOptions, $options); + + $this->addAutomaticStyle('UNICAEN_FIL_MP', 'paragraph', [ + 'style:text-properties' => [ + 'style:font-name' => $options['font-name'], + 'fo:font-size' => '1pt', + ], + 'loext:graphic-properties' => [ + 'draw:fill' => 'solid', + 'draw:fill-color' => $options['color'], + 'draw:opacity' => round($options['opacity'] * 100) . '%', + ], + ]); + + $this->addAutomaticStyle('UNICAEN_FIL_MGR', 'graphic', [ + 'style:graphic-properties' => [ + 'draw:stroke' => 'none', + 'draw:fill' => 'solid', + 'draw:fill-color' => $options['color'], + 'draw:opacity' => round($options['opacity'] * 100) . '%', + 'draw:auto-grow-height' => 'false', + 'draw:auto-grow-width' => 'false', + 'fo:min-height' => $options['width'] . 'cm', + 'fo:min-width' => $options['height'] . 'cm', + 'style:run-through' => 'background', + 'style:wrap' => 'run-through', + 'style:number-wrapped-paragraphs' => 'no-limit', + 'style:vertical-pos' => 'middle', + 'style:vertical-rel' => 'page-content', + 'style:horizontal-pos' => 'center', + 'style:horizontal-rel' => 'page-content', + 'draw:wrap-influence-on-position' => 'once-concurrent', + 'style:flow-with-text' => 'false', + ], + ]); + + $masterPages = $this->find('style:master-page'); + foreach ($masterPages as $masterPage) { + $fh = $this->findFrom($masterPage, 'style:header'); + if ($fh->length == 1) { + $header = $fh->item(0); + } else { + $header = $this->newElement('style:header'); + $masterPage->appendChild($header); + } + + $p1 = $this->newElement('text:p', [ + 'text:style-name' => 'Header'] + ); + $draw1 = $this->newElement('draw:custom-shape', [ + 'text:anchor-type' => "char", + 'draw:z-index' => "0", + 'draw:name' => "PowerPlusWaterMarkObject", + 'draw:style-name' => "UNICAEN_FIL_MGR", + 'draw:text-style-name' => "UNICAEN_FIL_MP", + 'svg:width' => $options['width'] . 'cm', + 'svg:height' => $options['height'] . 'cm', + 'draw:transform' => "rotate (0.785398163397448) translate (-0.134055555555556cm 15.9845416666667cm)", + ]); + $p2 = $this->newElement('text:p'); + $p2->nodeValue = $text; + + $draw2 = $this->newElement('draw:enhanced-geometry', [ + 'svg:viewBox' => "0 0 21600 21600", + 'draw:text-areas' => "0 0 21600 21600", + 'draw:text-path' => "true", + 'draw:type' => "fontwork-plain-text", + 'draw:modifiers' => "10800", + 'draw:enhanced-path' => "M ?f3 0 L ?f5 0 N M ?f6 21600 L ?f7 21600 N", + ]); + + $equations = [ + 'f0' => '$0 -10800', + 'f1' => '?f0 *2', + 'f2' => 'abs(?f1 )', + 'f3' => 'if(?f1 ,0,?f2 )', + 'f4' => '21600-?f2', + 'f5' => 'if(?f1 ,?f4 ,21600)', + 'f6' => 'if(?f1 ,?f2 ,0)', + 'f7' => 'if(?f1 ,21600,?f4 )', + ]; + foreach ($equations as $eqn => $eqf) { + $eqNode = $this->newElement('draw:equation', [ + 'draw:name' => $eqn, + 'draw:formula' => $eqf, + ]); + $draw2->appendChild($eqNode); + } + + $handle = $this->newElement('draw:handle', [ + 'draw:handle-position' => '$0 21600', + 'draw:handle-range-x-minimum' => '6629', + 'draw:handle-range-x-maximum' => '14971', + ]); + $draw2->appendChild($handle); + + $header + ->appendChild($p1) + ->appendChild($draw1) + ->appendChild($p2); + + $draw1->appendChild($draw2); + } + $this->getDocument()->setStylesChanged(true); + + return $this; + } + + + + /** + * @param string $name + * + * @return DOMNode[] + * @throws Exception + */ + private function find(string $name): DOMNodeList + { + $document = $this->getDocument(); + + return $document->find($document->getStyles(), $name); + } + + + + /** + * @param DOMNode $node + * @param $name + * + * @return DOMNodeList + * @throws Exception + */ + private function findFrom(DOMNode $node, $name): DOMNodeList + { + return $this->getDocument()->find($node, $name); + } + + + + /** + * @param string $name + * @param array $attrs + * + * @return DOMElement + */ + private function newElement(string $name, array $attrs = []): DOMElement + { + $document = $this->getDocument(); + + return $document->newElement($document->getStyles(), $name, $attrs); + } +} \ No newline at end of file