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