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