From 861f45e9a8595503d9960a41b4d4ca42ab728a9e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laurent=20L=C3=A9cluse?= <laurent.lecluse@unicaen.fr>
Date: Wed, 30 Jan 2019 14:12:58 +0100
Subject: [PATCH] Pb colier/coller de fichier!!

---
 src/Document.php  | 878 +++++++++++++++++++++++++++++-----------------
 src/Publisher.php |   3 +-
 2 files changed, 559 insertions(+), 322 deletions(-)

diff --git a/src/Document.php b/src/Document.php
index 286e6ad..a6f8e15 100644
--- a/src/Document.php
+++ b/src/Document.php
@@ -2,83 +2,196 @@
 
 namespace Unicaen\OpenDocument;
 
-use Exception;
 use DOMDocument;
 use DOMElement;
 use DOMNode;
 use DOMNodeList;
+use Exception;
 use ZipArchive;
 
-class Publisher
+
+class Document
 {
-    const PAGE_BREAK_NAME = 'UNICAEN_PAGE_BREAK';
 
-    const PARAGRAPH = 'text:p';
-    const TABLE_ROW = 'table:table-row';
+    /**
+     * @var ZipArchive
+     */
+    private $archive;
 
     /**
-     * Lecteur de fichier OpenDocument
-     *
-     * @var Document
+     * @var bool
      */
-    private $document;
+    private $pdfOutput = false;
+
+    /**
+     * @var Publisher
+     */
+    private $publisher;
+
+    /**
+     * @var Stylist
+     */
+    private $stylist;
+
+    /**
+     * @var DomDocument;
+     */
+    private $meta;
+
+    /**
+     * @var DOMDocument
+     */
+    private $styles;
 
     /**
-     * Contenu XML du corps de texte
-     *
      * @var DOMDocument
      */
     private $content;
 
     /**
-     * @var DOMElement
+     * @var array
      */
-    private $body;
+    private $namespaces = [];
 
     /**
-     * Ajoute un saut de page automatiquement entre deux instances de document lors du publipostage
-     *
-     * @var boolean
+     * @var bool
      */
-    private $autoBreak = true;
+    private $metaChanged = false;
 
     /**
-     * @var array
+     * @var bool
      */
-    private $values = [];
+    private $stylesChanged = false;
+
+    /**
+     * @var bool
+     */
+    private $contentChanged = false;
 
     /**
-     * Variable contenant le résultat final du content.xml
-     *
      * @var string
      */
-    private $outContent;
+    private $convCommand = 'unoconv -f pdf -o :outputFile :inputFile';
 
     /**
-     * @var bool
+     * @var string
      */
-    private $published = false;
+    private $tmpDir;
 
+    /**
+     * @var array
+     */
+    private $formatters = [];
 
 
     /**
-     * @return Document
+     * @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 getDocument(): Document
+    public function getStyles(): DOMDocument
     {
-        return $this->document;
+        return $this->styles;
     }
 
 
 
     /**
-     * @param Document $document
+     * @return DOMDocument
+     */
+    public function getMeta(): DOMDocument
+    {
+        return $this->meta;
+    }
+
+
+
+    /**
+     * @return bool
+     */
+    public function isPdfOutput(): bool
+    {
+        return $this->pdfOutput;
+    }
+
+
+
+    /**
+     * @param bool $pdfOutput
      *
-     * @return Publisher
+     * @return Document
      */
-    public function setDocument(Document $document): Publisher
+    public function setPdfOutput(bool $pdfOutput): Document
     {
-        $this->document = $document;
+        $this->pdfOutput = $pdfOutput;
 
         return $this;
     }
@@ -86,23 +199,23 @@ class Publisher
 
 
     /**
-     * @return DOMElement
+     * @return bool
      */
-    public function getBody(): DOMElement
+    public function isMetaChanged(): bool
     {
-        return $this->body;
+        return $this->metaChanged;
     }
 
 
 
     /**
-     * Peuple l'instance courante du document et l'ajoute à la suite du fichier
+     * @param bool $metaChanged
      *
-     * @param array $values
+     * @return Document
      */
-    public function add(array $values): Publisher
+    public function setMetaChanged(bool $metaChanged): Document
     {
-        $this->values[] = $values;
+        $this->metaChanged = $metaChanged;
 
         return $this;
     }
@@ -110,21 +223,23 @@ class Publisher
 
 
     /**
-     * @return array
+     * @return bool
      */
-    public function getValues(): array
+    public function isStylesChanged(): bool
     {
-        return $this->values;
+        return $this->stylesChanged;
     }
 
 
 
     /**
-     * @param array $values
+     * @param bool $stylesChanged
+     *
+     * @return Document
      */
-    public function setValues(array $values): Publisher
+    public function setStylesChanged(bool $stylesChanged): Document
     {
-        $this->values = $values;
+        $this->stylesChanged = $stylesChanged;
 
         return $this;
     }
@@ -134,21 +249,21 @@ class Publisher
     /**
      * @return bool
      */
-    public function isAutoBreak(): bool
+    public function isContentChanged(): bool
     {
-        return $this->autoBreak;
+        return $this->contentChanged;
     }
 
 
 
     /**
-     * @param bool $autoBreak
+     * @param bool $contentChanged
      *
-     * @return Publisher
+     * @return Document
      */
-    public function setAutoBreak(bool $autoBreak): Publisher
+    public function setContentChanged(bool $contentChanged): Document
     {
-        $this->autoBreak = $autoBreak;
+        $this->contentChanged = $contentChanged;
 
         return $this;
     }
@@ -156,50 +271,167 @@ class Publisher
 
 
     /**
-     * @param DOMElement $element
+     * @return string
+     */
+    public function getConvCommand(): string
+    {
+        return $this->convCommand;
+    }
+
+
+
+    /**
+     * @param string $convCommand
      *
-     * @return Publisher
-     * @throws Exception
+     * @return Document
      */
-    public function getVariables(DOMElement $element): array
+    public function setConvCommand(string $convCommand): Document
     {
-        $textNs = $this->getDocument()->getNamespaceUrl('text');
+        $this->convCommand = $convCommand;
+
+        return $this;
+    }
 
-        $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;
+
+    /**
+     * 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 $variables;
+        return $m;
     }
 
 
 
-    public function getSections(DOMElement $element): array
+    /**
+     * @param string $variable
+     * @param mixed $value
+     *
+     * @return string
+     */
+    public function formatValue(string $variable, $value): string
     {
-        $textNs = $this->getDocument()->getNamespaceUrl('text');
+        if (isset($this->formatters[$variable]) && $format = $this->formatters[$variable]){
+            if (is_callable($format)){
+                $value = $format($value);
+            }
+        }
+
+        if (is_float($value)){
+            return number_format($value, 2, ',', ' ');
+        }
+
+        return (string)$value;
+    }
 
-        $sections  = [];
-        $vElements = $this->findFrom($element, 'text:section');
-        foreach ($vElements as $vElement) {
-            $name = $vElement->getAttributeNS($textNs, 'name');
 
-            if (!isset($sections[$name])) $sections[$name] = [];
-            $sections[$name][] = $vElement;
+
+    /**
+     * 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 $sections;
+        return $this->namespaces;
     }
 
 
 
-    public function hideSection(DOMElement $section): Publisher
+    /**
+     * @param string $name
+     *
+     * @return bool
+     */
+    public function hasNamespace(string $name): bool
     {
-        $section->parentNode->removeChild($section);
+        $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;
     }
@@ -207,70 +439,73 @@ class Publisher
 
 
     /**
-     * @param DOMElement $element
-     * @param mixed      $value
+     * Retourne l'url associé à un espace de nom
      *
-     * @return Publisher
+     * @param string $namespace
+     *
+     * @return string
      */
-    protected function setVariable(DOMElement $element, $value): Publisher
+    public function getNamespaceUrl(string $namespace): string
     {
-        $textNs = $this->getDocument()->getNamespaceUrl('text');
-        $document = $element->ownerDocument;
+        $ns = $this->getNamespaces();
 
-        if (is_string($value) && false !== strpos($value, "\n")){
-            $value = explode("\n", $value);
-        }else{
-            $value = (array)$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($this->getDocument()->formatValue($element->nodeValue,$value[$i]));
-            $element->parentNode->insertBefore($vText, $element);
+        if (!isset($ns[$namespace])) {
+            throw new Exception('L\'espace de nom ' . $namespace . ' n\'a pas été trouvé.');
         }
-        $element->parentNode->removeChild($element);
 
-        return $this;
+        return $ns[$namespace];
     }
 
 
 
-    private function addPageBreakStyle(): Publisher
+    /**
+     * Retourne un champ d'information
+     *
+     * @param integer $index
+     *
+     * @return string
+     */
+    public function getInfo($index)
     {
-        /* get office:automatic-styles node */
-        $styles = $this->content->getElementsByTagNameNS(
-            $this->getDocument()->getNamespaceUrl('office'),
-            'automatic-styles'
-        )->item(0);
+        $infonodes = $this->getMeta()->getElementsByTagNameNS($this->getNamespaceUrl('meta'), 'user-defined');
 
-        $stylePageBreak           = $this->newElement('style:style', [
-            'style:name'              => self::PAGE_BREAK_NAME,
-            'style:family'            => 'paragraph',
-            'style:parent-style-name' => 'Standard',
-            'style:master-page-name'  => 'Standard',
-        ]);
-        $stylePageBreakProperties = $this->newElement('style:paragraph-properties', [
-            'style:writing-mode' => 'page',
-            'style:page-number'  => '1',
-        ]);
+        return $infonodes->item($index)->nodeValue;
+    }
 
-        $stylePageBreak->appendChild($stylePageBreakProperties);
-        $styles->appendChild($stylePageBreak);
+
+
+    /**
+     * 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;
     }
 
 
 
-    private function addPageBreak(DOMElement $element): Publisher
+    /**
+     * @param array $values
+     *
+     * @return Document
+     */
+    public function publish(array $values): Document
     {
-        $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);
+        $this->getPublisher()->setValues($values);
+        $this->getPublisher()->publish();
 
         return $this;
     }
@@ -279,161 +514,153 @@ class Publisher
 
     /**
      * @return Publisher
-     * @throws Exception
      */
-    public function publishBegin(): Publisher
+    public function getPublisher(): Publisher
     {
-        /* On récupère le content du document */
-        $this->content = new DOMDocument();
-        $this->content->loadXML($this->getDocument()->getContent()->saveXML());
-
-        if ($this->isAutoBreak()) {
-            $this->addPageBreakStyle();
+        if (!$this->publisher) {
+            $this->publisher = new Publisher();
+            $this->publisher->setDocument($this);
         }
 
-        $contentText              = $this->content->saveXML();
-        $officeDocumentContentPos = strpos($contentText, '<office:document-content');
-        $length                   = strpos($contentText, '>', $officeDocumentContentPos) + 1;
-        $this->out(substr($contentText, 0, $length));
+        return $this->publisher;
+    }
 
-        /* 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));
-            }
-        }
 
-        $this->body = $this->content->getElementsByTagNameNS($this->getDocument()->getNamespaceUrl('office'), 'text')[0];
+    /**
+     * @return Stylist
+     */
+    public function getStylist(): Stylist
+    {
+        if (!$this->stylist) {
+            $this->stylist = new Stylist();
+            $this->stylist->setDocument($this);
+        }
 
-        return $this;
+        return $this->stylist;
     }
 
 
 
     /**
-     * @return Publisher
+     * @param $data
+     *
+     * @return Document
+     * @throws Exception
      */
-    public function publishEnd(): Publisher
+    public function loadFromData($data): Document
     {
-        $this->out("</office:body></office:document-content>");
+        if (!class_exists('ZipArchive')) {
+            throw new Exception('Zip extension not loaded');
+        }
 
-        /* On renvoie le content dans le document */
-        $this->getDocument()->getArchive()->addFromString('content.xml', $this->outContent);
-        $this->published = true;
+        $odtFile = $this->tempFileName('odtfile_', 'odt');
+        file_put_contents($odtFile, $data);
 
-        return $this;
+        return $this->loadFromFile($odtFile, false);
     }
 
 
 
     /**
-     * @param DOMElement $element
-     * @param array      $values
+     * @param string $fileName
+     * @param bool   $duplicate
      *
-     * @return Publisher
+     * @return Document
      * @throws Exception
      */
-    public function publishValues(DOMElement $element, array $values): Publisher
+    public function loadFromFile(string $fileName, bool $duplicate = true): Document
     {
-        if ($element === $this->body) {
-            $this->getDocument()->getStylist()->setVariables($values);
+        if (!class_exists('ZipArchive')) {
+            throw new Exception('Zip extension not loaded');
         }
 
-        $variables = $this->getVariables($element);
-        $sections  = $this->getSections($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);
-                    }
-                }
-                if (isset($sections[$vname])) {
-                    foreach ($sections[$vname] as $elSec) {
-                        $this->publishSubData($elSec, $vparent, $val);
-                    }
-                }
-            } else {
-                if (isset($variables[$name])) {
-                    foreach ($variables[$name] as $vElement) {
-                        $this->setVariable($vElement, $val ? $val : '');
-                    }
-                }
-                if (false !== strpos($name, '@')) {
-                    list($sName, $sType) = explode("@", $name);
-                    if ($sType == 'text:section') {
-                        $name = $sName;
-                    }
-                }
-                if (isset($sections[$name])) {
-                    foreach ($sections[$name] as $sElement) {
-                        if ($val === null
-                            || $val === 0
-                            || $val === '0'
-                            || $val === false
-                            || $val === ''
-                            || strtolower($val) === 'false'
-                        ) {
-                            $this->hideSection($sElement);
-                        }
-                    }
-                }
-            }
+        if (!file_exists($fileName)) {
+            throw new Exception('OpenDocument file "' . $fileName . '" doesn\'t exists.');
         }
 
-        if ($element === $this->body) {
-            $this->out($this->content->saveXML($element));
+        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;
     }
 
 
 
     /**
-     * @param DOMElement $element
-     * @param string     $parent
-     * @param string     $variable
-     *
-     * @return DOMElement
+     * @return string
      * @throws Exception
      */
-    public function getSubDoc(DOMElement $element, string $parent, string $variable): DOMElement
+    private function prepareSaving($filename = null): string
     {
-        $variables = $this->getVariables($element);
-        if (!isset($variables[$variable][0])) {
-            throw new \Exception('La variable "' . $variable . '"" n\'a pas été trouvée dans le document');
+        if ($this->isMetaChanged()) {
+            $this->getArchive()->addFromString('meta.xml', $this->getMeta()->saveXML());
         }
 
-        return $this->getSubDocWithVariable($variables[$variable][0], $parent);
+        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 DOMElement $element
+     * @param $origine
+     * @param $destination
      *
-     * @return Publisher
+     * @return Document
+     * @throws Exception
      */
-    public function remove(DOMElement $element): Publisher
+    public function odtToPdf($origine, $destination): Document
     {
-        $element->parentNode->removeChild($element);
+        $command = $this->getConvCommand();
+
+        $command = str_replace(':inputFile', $origine, $this->getConvCommand());
+        $command = str_replace(':outputFile', $destination, $command);
+        exec($command, $output, $return);
+
+        if (0 != $return) {
+            throw new \Exception('La conversion de document en PDF a échoué');
+        }
 
         return $this;
     }
@@ -441,17 +668,14 @@ class Publisher
 
 
     /**
-     * @param DOMElement $subDoc
-     * @param array      $variables
-     * @param DOMElement $refNode
+     * @param string $fileName
      *
-     * @return Publisher
+     * @return Document
+     * @throws Exception
      */
-    public function publishBefore(DOMElement $subDoc, array $variables, DOMElement $refNode): Publisher
+    public function saveToFile(string $fileName): Document
     {
-        $node = $subDoc->cloneNode(true);
-        $this->publishValues($node, $variables);
-        $refNode->parentNode->insertBefore($node, $refNode);
+        $actualFile = $this->prepareSaving($fileName);
 
         return $this;
     }
@@ -459,174 +683,186 @@ class Publisher
 
 
     /**
-     * @param DOMElement $element
-     * @param string     $parent
-     * @param DOMElement $variable
-     *
-     * @return DOMElement
+     * @return string
      * @throws Exception
      */
-    private function getSubDocWithVariable(DOMElement $element, string $parent): DOMElement
+    public function saveToData(): string
     {
-        if ($element->tagName == 'text:section') {
-            return $element; // C'est la section qui est dupliquée
-        } else {
-            $i     = 10;
-            $found = false;
-            for ($i = 0; $i < 10; $i++) {
-                $parentNode = isset($parentNode) ? $parentNode->parentNode : $element->parentNode;
-                if ($parentNode->nodeName == $parent) {
-                    $found = true;
-                    break;
-                }
-            }
+        $actualFile = $this->prepareSaving();
 
-            if (!$found) {
-                throw new \Exception('Le noeud parent de type ' . $parent . ' n\'a pas été trouvé');
-            }
-
-            return $parentNode;
-        }
+        return file_get_contents($actualFile);
     }
 
 
 
     /**
-     * @param DOMElement $element
-     * @param string     $parent
-     * @param array      $values
-     *
-     * @return Publisher
+     * @return string
      */
-    private function publishSubData(DOMElement $element, string $parent, array $values): Publisher
+    public function getTmpDir(): string
     {
-        $parentNode = $this->getSubDocWithVariable($element, $parent);
-
-        foreach ($values as $vals) {
-            $clone = $parentNode->cloneNode(true);
-            $this->publishValues($clone, $vals);
-            $parentNode->parentNode->insertBefore($clone, $parentNode);
+        if (!$this->tmpDir) {
+            return sys_get_temp_dir();
         }
 
-        return $this->remove($parentNode);
+        return $this->tmpDir;
     }
 
 
 
     /**
-     * Construit le fichier final à partir des données
+     * @param string $tmpDir
+     *
+     * @return Document
      */
-    public function publish()
+    public function setTmpDir(string $tmpDir): Document
     {
-        $this->publishBegin();
-
-        $first = true;
-        foreach ($this->values as $values) {
-            $bodyNode = $this->body->cloneNode(true);
-            if ($first) {
-                $first = false;
-            } elseif ($this->isAutoBreak()) {
-                $this->addPageBreak($bodyNode);
-            }
-
-            $this->publishValues($bodyNode, $values);
-            $this->getDocument()->getStylist()->setVariables($values);
-            $this->out($this->content->saveXML($bodyNode));
-            $first = false;
+        $this->tmpDir = $tmpDir;
+        if (!file_exists($tmpDir)) {
+            mkdir($tmpDir);
         }
 
-        $this->publishEnd();
+        return $this;
     }
 
 
 
     /**
-     * @param string $name
+     * @param null $prefix
      *
-     * @return DOMNode[]
-     * @throws Exception
+     * @return string
      */
-    private function find(string $name): DOMNodeList
+    private function tempFileName($prefix = null, $ext = 'odt'): string
     {
-        $document = $this->getDocument();
+        $tmpDir = $this->getTmpDir();
+        if ('/' != substr($tmpDir, -1)) {
+            $tmpDir .= '/';
+        }
+
+        $tempFileName     = uniqid($tmpDir . $prefix) . '.' . $ext;
+        $this->tmpFiles[] = $tempFileName;
 
-        return $document->find($this->content, $name);
+        return $tempFileName;
     }
 
 
 
     /**
-     * @param DOMNode $node
-     * @param         $name
-     *
-     * @return DOMNodeList
-     * @throws Exception
+     * @return array
      */
-    private function findFrom(DOMNode $node, $name): DOMNodeList
+    public function getFormatters(): array
     {
-        return $this->getDocument()->find($node, $name);
+        return $this->formatters;
     }
 
 
 
     /**
-     * @param string $name
-     * @param array  $attrs
+     * @param string $variable
+     * @param mixed $format
      *
-     * @return DOMElement
+     * @return Document
      */
-    private function newElement(string $name, array $attrs = []): DOMElement
+    public function addFormatter(string $variable, $format): Document
     {
-        $document = $this->getDocument();
+        $this->formatters[$variable] = $format;
 
-        return $document->newElement($this->content, $name, $attrs);
+        return $this;
     }
 
 
 
     /**
-     * Ajoute du contenu au fichier content.xml
-     * Méthode à ne pas exploiter
+     * 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.
      *
-     * @param string $xml
+     * 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 out($xml)
+    public function __destruct()
     {
-        $this->outContent .= $xml;
+        /* On supprime les éventuels fichiers temporaires pour libérer l'espace */
+        foreach ($this->tmpFiles as $tmpFile) {
+            if (file_exists($tmpFile)) {
+                unlink($tmpFile);
+            }
+        }
     }
 
 
 
     /**
-     * @return string
+     * @param $fileName
+     *
+     * @return Document
+     * @throws Exception
      */
-    public function getOutContent(): string
+    public function download($fileName): Document
     {
-        return $this->outContent;
+        $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;
     }
 
 
 
     /**
-     * @return bool
+     * @param DOMNode $node
+     * @param string  $name
+     *
+     * @return DOMNode[]
+     * @throws Exception
      */
-    public function isPublished(): bool
+    public function find(DOMNode $node, string $name): DOMNodeList
     {
-        return $this->published;
+        list($namespace, $name) = explode(':', $name);
+
+        return $node->getElementsByTagNameNS($this->getNamespaceUrl($namespace), $name);
     }
 
 
 
     /**
-     * @param bool $published
+     * @param DOMDocument $document
+     * @param string      $name
+     * @param array       $attrs
      *
-     * @return Publisher
+     * @return DOMElement
+     * @throws Exception
      */
-    public function setPublished(bool $published): Publisher
+    public function newElement(DOMDocument $document, string $name, array $attrs = []): DOMElement
     {
-        $this->published = $published;
+        list($namespace) = explode(':', $name);
 
-        return $this;
-    }
+        $newNode = $document->createElementNS($this->getNamespaceUrl($namespace), $name);
+        foreach ($attrs as $attrName => $attrValue) {
+            list($attrNS) = explode(':', $attrName);
+            $newNode->setAttributeNS($this->getNamespaceUrl($attrNS), $attrName, $attrValue);
+        }
 
-}
\ No newline at end of file
+        return $newNode;
+    }
+}
diff --git a/src/Publisher.php b/src/Publisher.php
index 22fd950..286e6ad 100644
--- a/src/Publisher.php
+++ b/src/Publisher.php
@@ -505,7 +505,7 @@ class Publisher
         foreach ($values as $vals) {
             $clone = $parentNode->cloneNode(true);
             $this->publishValues($clone, $vals);
-            $this->insertBefore($clone, $parentNode);
+            $parentNode->parentNode->insertBefore($clone, $parentNode);
         }
 
         return $this->remove($parentNode);
@@ -530,6 +530,7 @@ class Publisher
             }
 
             $this->publishValues($bodyNode, $values);
+            $this->getDocument()->getStylist()->setVariables($values);
             $this->out($this->content->saveXML($bodyNode));
             $first = false;
         }
-- 
GitLab