diff --git a/CHANGELOG.md b/CHANGELOG.md index d00f16c7..03400d46 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ This release marked heavy refactorings on internal code structure with the creat - Reader: Ability to read header, footer, footnotes, link, preservetext, textbreak, pagebreak, table, and list - @ivanlanin - Endnote: Ability to add endnotes - @ivanlanin - ListItem: Ability to create custom list and reset list number - @ivanlanin GH-10 GH-198 +- ODT Writer: Basic table writing support - @ivanlanin ### Bugfixes diff --git a/src/PhpWord/Element/AbstractElement.php b/src/PhpWord/Element/AbstractElement.php index b7a03934..890227dc 100644 --- a/src/PhpWord/Element/AbstractElement.php +++ b/src/PhpWord/Element/AbstractElement.php @@ -81,6 +81,20 @@ abstract class AbstractElement */ protected $elements = array(); + /** + * Index of element in the elements collection (start with 1) + * + * @var integer + */ + protected $elementIndex = 1; + + /** + * Unique Id for element + * + * @var integer + */ + protected $elementId; + /** * Relation Id * @@ -108,7 +122,7 @@ abstract class AbstractElement $text = String::toUTF8($text); $textObject = new Text($text, $fontStyle, $paragraphStyle); $textObject->setDocPart($this->getDocPart(), $this->getDocPartId()); - $this->elements[] = $textObject; + $this->addElement($textObject); return $textObject; } @@ -125,7 +139,7 @@ abstract class AbstractElement $textRun = new TextRun($paragraphStyle); $textRun->setDocPart($this->getDocPart(), $this->getDocPartId()); - $this->elements[] = $textRun; + $this->addElement($textRun); return $textRun; } @@ -148,7 +162,7 @@ abstract class AbstractElement $link->setDocPart($this->getDocPart(), $this->getDocPartId()); $rId = Media::addElement($elementDocPart, 'link', $linkSrc); $link->setRelationId($rId); - $this->elements[] = $link; + $this->addElement($link); return $link; } @@ -179,7 +193,7 @@ abstract class AbstractElement $bookmarkId = $data[1]; $title->setAnchor($anchor); $title->setBookmarkId($bookmarkId); - $this->elements[] = $title; + $this->addElement($title); return $title; } @@ -198,7 +212,7 @@ abstract class AbstractElement $preserveText = new PreserveText(String::toUTF8($text), $fontStyle, $paragraphStyle); $preserveText->setDocPart($this->getDocPart(), $this->getDocPartId()); - $this->elements[] = $preserveText; + $this->addElement($preserveText); return $preserveText; } @@ -217,7 +231,7 @@ abstract class AbstractElement for ($i = 1; $i <= $count; $i++) { $textBreak = new TextBreak($fontStyle, $paragraphStyle); $textBreak->setDocPart($this->getDocPart(), $this->getDocPartId()); - $this->elements[] = $textBreak; + $this->addElement($textBreak); } } @@ -237,7 +251,7 @@ abstract class AbstractElement $listItem = new ListItem(String::toUTF8($text), $depth, $fontStyle, $styleList, $paragraphStyle); $listItem->setDocPart($this->getDocPart(), $this->getDocPartId()); - $this->elements[] = $listItem; + $this->addElement($listItem); return $listItem; } @@ -253,7 +267,7 @@ abstract class AbstractElement $this->checkValidity('table'); $table = new Table($this->getDocPart(), $this->getDocPartId(), $style); - $this->elements[] = $table; + $this->addElement($table); return $table; } @@ -275,7 +289,8 @@ abstract class AbstractElement $image->setDocPart($this->getDocPart(), $this->getDocPartId()); $rId = Media::addElement($elementDocPart, 'image', $src, $image); $image->setRelationId($rId); - $this->elements[] = $image; + $this->addElement($image); + return $image; } @@ -307,7 +322,8 @@ abstract class AbstractElement $object->setRelationId($rId); $rIdimg = Media::addElement($elementDocPart, 'image', $icon, new Image($icon)); $object->setImageRelationId($rIdimg); - $this->elements[] = $object; + $this->addElement($object); + return $object; } else { throw new InvalidObjectException(); @@ -329,7 +345,7 @@ abstract class AbstractElement $footnote->setDocPart('footnote', $this->getDocPartId()); $footnote->setRelationId($rId); - $this->elements[] = $footnote; + $this->addElement($footnote); return $footnote; } @@ -349,7 +365,7 @@ abstract class AbstractElement $endnote->setDocPart('endnote', $this->getDocPartId()); $endnote->setRelationId($rId); - $this->elements[] = $endnote; + $this->addElement($endnote); return $endnote; } @@ -369,7 +385,7 @@ abstract class AbstractElement $checkBox = new CheckBox(String::toUTF8($name), String::toUTF8($text), $fontStyle, $paragraphStyle); $checkBox->setDocPart($this->getDocPart(), $this->getDocPartId()); - $this->elements[] = $checkBox; + $this->addElement($checkBox); return $checkBox; } @@ -416,6 +432,16 @@ abstract class AbstractElement return $this->docPartId; } + /** + * Set element index and unique id, and add element into elements collection + */ + protected function addElement(AbstractElement $element) + { + $element->setElementIndex($this->countElements() + 1); + $element->setElementId(); + $this->elements[] = $element; + } + /** * Get all elements * @@ -426,6 +452,54 @@ abstract class AbstractElement return $this->elements; } + /** + * Count elements + * + * @return integer + */ + public function countElements() + { + return count($this->elements); + } + + /** + * Get element index + * + * @return int + */ + public function getElementIndex() + { + return $this->elementIndex; + } + + /** + * Set element index + * + * @param int $value + */ + public function setElementIndex($value) + { + $this->elementIndex = $value; + } + + /** + * Get element unique ID + * + * @return string + */ + public function getElementId() + { + return $this->elementId; + } + + /** + * Set element unique ID from 6 first digit of md5 + */ + public function setElementId() + { + $this->elementId = substr(md5(rand()), 0, 6); + } + /** * Get relation Id * diff --git a/src/PhpWord/Element/Table.php b/src/PhpWord/Element/Table.php index 90913582..3241a8e2 100644 --- a/src/PhpWord/Element/Table.php +++ b/src/PhpWord/Element/Table.php @@ -35,7 +35,7 @@ class Table extends AbstractElement /** * Table width * - * @var int + * @var integer */ private $width = null; @@ -44,7 +44,7 @@ class Table extends AbstractElement * Create a new table * * @param string $docPart - * @param int $docPartId + * @param integer $docPartId * @param mixed $style */ public function __construct($docPart, $docPartId, $style = null) @@ -56,7 +56,7 @@ class Table extends AbstractElement /** * Add a row * - * @param int $height + * @param integer $height * @param mixed $style */ public function addRow($height = null, $style = null) @@ -69,7 +69,7 @@ class Table extends AbstractElement /** * Add a cell * - * @param int $width + * @param integer $width * @param mixed $style * @return Cell */ @@ -103,7 +103,7 @@ class Table extends AbstractElement /** * Set table width * - * @param int $width + * @param integer $width */ public function setWidth($width) { @@ -113,10 +113,31 @@ class Table extends AbstractElement /** * Get table width * - * @return int + * @return integer */ public function getWidth() { return $this->width; } + + /** + * Get column count + * + * @return integer + */ + public function countColumns() + { + $columnCount = 0; + if (is_array($this->rows)) { + $rowCount = count($this->rows); + for ($i = 0; $i < $rowCount; $i++) { + $cellCount = count($this->rows[$i]->getCells()); + if ($columnCount < $cellCount) { + $columnCount = $cellCount; + } + } + } + + return $columnCount; + } } diff --git a/src/PhpWord/Writer/ODText/Content.php b/src/PhpWord/Writer/ODText/Content.php index 327aed34..e8f087fe 100644 --- a/src/PhpWord/Writer/ODText/Content.php +++ b/src/PhpWord/Writer/ODText/Content.php @@ -24,7 +24,6 @@ use PhpOffice\PhpWord\Shared\XMLWriter; use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style\Font; use PhpOffice\PhpWord\Style\Paragraph; -use PhpOffice\PhpWord\TOC; /** * ODText content part writer @@ -34,7 +33,6 @@ class Content extends Base /** * Write content file to XML format * - * @param PhpWord $phpWord * @return string XML Output */ public function writeContent(PhpWord $phpWord = null) @@ -95,10 +93,304 @@ class Content extends Base } } - // office:font-face-decls - $this->writeFontFaces($xmlWriter); - // office:automatic-styles + $this->writeFontFaces($xmlWriter); // office:font-face-decls + + $this->writeAutomaticStyles($xmlWriter); // office:automatic-styles + + // Tables + $sections = $phpWord->getSections(); + $countSections = count($sections); + if ($countSections > 0) { + $sectionId = 0; + foreach ($sections as $section) { + $sectionId++; + $elements = $section->getElements(); + foreach ($elements as $element) { + if ($elements instanceof Table) { + $objWriter->startElement('style:style'); + $objWriter->writeAttribute('style:name', $element->getElementId()); + $objWriter->writeAttribute('style:family', 'table'); + $objWriter->startElement('style:table-properties'); + //$objWriter->writeAttribute('style:width', 'table'); + $objWriter->writeAttribute('style:rel-width', 100); + $objWriter->writeAttribute('table:align', 'center'); + $objWriter->endElement(); + $objWriter->endElement(); + } + } + } + } + + $xmlWriter->endElement(); + + // office:body + $xmlWriter->startElement('office:body'); + // office:text + $xmlWriter->startElement('office:text'); + // text:sequence-decls + $xmlWriter->startElement('text:sequence-decls'); + // text:sequence-decl + $xmlWriter->startElement('text:sequence-decl'); + $xmlWriter->writeAttribute('text:display-outline-level', 0); + $xmlWriter->writeAttribute('text:name', 'Illustration'); + $xmlWriter->endElement(); + // text:sequence-decl + $xmlWriter->startElement('text:sequence-decl'); + $xmlWriter->writeAttribute('text:display-outline-level', 0); + $xmlWriter->writeAttribute('text:name', 'Table'); + $xmlWriter->endElement(); + // text:sequence-decl + $xmlWriter->startElement('text:sequence-decl'); + $xmlWriter->writeAttribute('text:display-outline-level', 0); + $xmlWriter->writeAttribute('text:name', 'Text'); + $xmlWriter->endElement(); + // text:sequence-decl + $xmlWriter->startElement('text:sequence-decl'); + $xmlWriter->writeAttribute('text:display-outline-level', 0); + $xmlWriter->writeAttribute('text:name', 'Drawing'); + $xmlWriter->endElement(); + $xmlWriter->endElement(); + + $sections = $phpWord->getSections(); + $countSections = count($sections); + if ($countSections > 0) { + foreach ($sections as $section) { + $elements = $section->getElements(); + + foreach ($elements as $element) { + if ($element instanceof Text) { + $this->writeText($xmlWriter, $element); + } elseif ($element instanceof TextRun) { + $this->writeTextRun($xmlWriter, $element); + } elseif ($element instanceof Link) { + $this->writeLink($xmlWriter, $element); + } elseif ($element instanceof Title) { + $this->writeTitle($xmlWriter, $element); + } elseif ($element instanceof ListItem) { + $this->writeListItem($xmlWriter, $element); + } elseif ($element instanceof TextBreak) { + $this->writeTextBreak($xmlWriter); + } elseif ($element instanceof PageBreak) { + $this->writePageBreak($xmlWriter); + } elseif ($element instanceof Table) { + $this->writeTable($xmlWriter, $element); + } elseif ($element instanceof Image) { + $this->writeImage($xmlWriter, $element); + } elseif ($element instanceof Object) { + $this->writeObject($xmlWriter, $element); + } + } + } + } + $xmlWriter->endElement(); + $xmlWriter->endElement(); + $xmlWriter->endElement(); + + // Return + return $xmlWriter->getData(); + } + + /** + * Write text + * + * @param bool $withoutP + */ + protected function writeText(XMLWriter $xmlWriter, Text $text, $withoutP = false) + { + $styleFont = $text->getFontStyle(); + $styleParagraph = $text->getParagraphStyle(); + + // @todo Commented for TextRun. Should really checkout this value + // $SfIsObject = ($styleFont instanceof Font) ? true : false; + $SfIsObject = false; + + if ($SfIsObject) { + // Don't never be the case, because I browse all sections for cleaning all styles not declared + die('PhpWord : $SfIsObject wouldn\'t be an object'); + } else { + if (!$withoutP) { + $xmlWriter->startElement('text:p'); // text:p + } + if (empty($styleFont)) { + if (empty($styleParagraph)) { + $xmlWriter->writeAttribute('text:style-name', 'P1'); + } elseif (is_string($styleParagraph)) { + $xmlWriter->writeAttribute('text:style-name', $styleParagraph); + } + $xmlWriter->writeRaw($text->getText()); + } else { + if (empty($styleParagraph)) { + $xmlWriter->writeAttribute('text:style-name', 'Standard'); + } elseif (is_string($styleParagraph)) { + $xmlWriter->writeAttribute('text:style-name', $styleParagraph); + } + // text:span + $xmlWriter->startElement('text:span'); + if (is_string($styleFont)) { + $xmlWriter->writeAttribute('text:style-name', $styleFont); + } + $xmlWriter->writeRaw($text->getText()); + $xmlWriter->endElement(); + } + if (!$withoutP) { + $xmlWriter->endElement(); // text:p + } + } + } + + /** + * Write TextRun section + * + * @todo Enable all other section types + */ + protected function writeTextRun(XMLWriter $xmlWriter, TextRun $textrun) + { + $elements = $textrun->getElements(); + $xmlWriter->startElement('text:p'); + if (count($elements) > 0) { + foreach ($elements as $element) { + if ($element instanceof Text) { + $this->writeText($xmlWriter, $element, true); + } + } + } + $xmlWriter->endElement(); + } + + /** + * Write link element + */ + protected function writeLink(XMLWriter $xmlWriter, Link $link) + { + $this->writeUnsupportedElement($xmlWriter, 'Link'); + } + + /** + * Write title element + */ + protected function writeTitle(XMLWriter $xmlWriter, Title $title) + { + $this->writeUnsupportedElement($xmlWriter, 'Title'); + } + + /** + * Write preserve text + */ + protected function writePreserveText(XMLWriter $xmlWriter, PreserveText $preservetext) + { + $this->writeUnsupportedElement($xmlWriter, 'PreserveText'); + } + + /** + * Write list item + */ + protected function writeListItem(XMLWriter $xmlWriter, ListItem $listItem) + { + $this->writeUnsupportedElement($xmlWriter, 'ListItem'); + } + + /** + * Write text break + */ + protected function writeTextBreak(XMLWriter $xmlWriter) + { + $xmlWriter->startElement('text:p'); + $xmlWriter->writeAttribute('text:style-name', 'Standard'); + $xmlWriter->endElement(); + } + + /** + * Write page break + */ + protected function writePageBreak(XMLWriter $xmlWriter) + { + $this->writeUnsupportedElement($xmlWriter, 'PageBreak'); + } + + /** + * Write table + */ + protected function writeTable(XMLWriter $xmlWriter, Table $table) + { + $rows = $table->getRows(); + $rowCount = count($rows); + $colCount = $table->countColumns(); + if ($rowCount > 0) { + $xmlWriter->startElement('table:table'); + $xmlWriter->writeAttribute('table:name', $table->getElementId()); + $xmlWriter->writeAttribute('table:style', $table->getElementId()); + + $xmlWriter->startElement('table:table-column'); + $xmlWriter->writeAttribute('table:number-columns-repeated', $colCount); + $xmlWriter->endElement(); // table:table-column + + foreach ($rows as $row) { + $xmlWriter->startElement('table:table-row'); + foreach ($row->getCells() as $cell) { + $xmlWriter->startElement('table:table-cell'); + $xmlWriter->writeAttribute('office:value-type', 'string'); + $elements = $cell->getElements(); + if (count($elements) > 0) { + foreach ($elements as $element) { + if ($element instanceof Text) { + $this->writeText($xmlWriter, $element); + } elseif ($element instanceof TextRun) { + $this->writeTextRun($xmlWriter, $element); + } elseif ($element instanceof ListItem) { + $this->writeListItem($xmlWriter, $element); + } elseif ($element instanceof TextBreak) { + $this->writeTextBreak($xmlWriter); + } elseif ($element instanceof Image) { + $this->writeImage($xmlWriter, $element); + } elseif ($element instanceof Object) { + $this->writeObject($xmlWriter, $element); + } + } + } else { + $this->writeTextBreak($xmlWriter); + } + $xmlWriter->endElement(); // table:table-cell + } + $xmlWriter->endElement(); // table:table-row + } + $xmlWriter->endElement(); // table:table + } + } + + /** + * Write image + */ + protected function writeImage(XMLWriter $xmlWriter, Image $element) + { + $this->writeUnsupportedElement($xmlWriter, 'Image'); + } + + /** + * Write object + */ + protected function writeObject(XMLWriter $xmlWriter, Object $element) + { + $this->writeUnsupportedElement($xmlWriter, 'Object'); + } + + /** + * Write unsupported element + * + * @param string $element + */ + private function writeUnsupportedElement(XMLWriter $xmlWriter, $element) + { + $xmlWriter->startElement('text:p'); + $xmlWriter->writeRaw($element); + $xmlWriter->endElement(); + } + + /** + * Write automatic styles + */ + private function writeAutomaticStyles(XMLWriter $xmlWriter) + { $xmlWriter->startElement('office:automatic-styles'); $styles = Style::getStyles(); $numPStyles = 0; @@ -151,172 +443,5 @@ class Content extends Base $xmlWriter->endElement(); } } - $xmlWriter->endElement(); - - // office:body - $xmlWriter->startElement('office:body'); - // office:text - $xmlWriter->startElement('office:text'); - // text:sequence-decls - $xmlWriter->startElement('text:sequence-decls'); - // text:sequence-decl - $xmlWriter->startElement('text:sequence-decl'); - $xmlWriter->writeAttribute('text:display-outline-level', 0); - $xmlWriter->writeAttribute('text:name', 'Illustration'); - $xmlWriter->endElement(); - // text:sequence-decl - $xmlWriter->startElement('text:sequence-decl'); - $xmlWriter->writeAttribute('text:display-outline-level', 0); - $xmlWriter->writeAttribute('text:name', 'Table'); - $xmlWriter->endElement(); - // text:sequence-decl - $xmlWriter->startElement('text:sequence-decl'); - $xmlWriter->writeAttribute('text:display-outline-level', 0); - $xmlWriter->writeAttribute('text:name', 'Text'); - $xmlWriter->endElement(); - // text:sequence-decl - $xmlWriter->startElement('text:sequence-decl'); - $xmlWriter->writeAttribute('text:display-outline-level', 0); - $xmlWriter->writeAttribute('text:name', 'Drawing'); - $xmlWriter->endElement(); - $xmlWriter->endElement(); - - $sections = $phpWord->getSections(); - $countSections = count($sections); - if ($countSections > 0) { - foreach ($sections as $section) { - $elements = $section->getElements(); - - foreach ($elements as $element) { - if ($element instanceof Text) { - $this->writeText($xmlWriter, $element); - } elseif ($element instanceof TextRun) { - $this->writeTextRun($xmlWriter, $element); - } elseif ($element instanceof TextBreak) { - $this->writeTextBreak($xmlWriter); - } elseif ($element instanceof Link) { - $this->writeUnsupportedElement($xmlWriter, 'Link'); - } elseif ($element instanceof Title) { - $this->writeUnsupportedElement($xmlWriter, 'Title'); - } elseif ($element instanceof PageBreak) { - $this->writeUnsupportedElement($xmlWriter, 'Page Break'); - } elseif ($element instanceof Table) { - $this->writeUnsupportedElement($xmlWriter, 'Table'); - } elseif ($element instanceof ListItem) { - $this->writeUnsupportedElement($xmlWriter, 'List Item'); - } elseif ($element instanceof Image) { - $this->writeUnsupportedElement($xmlWriter, 'Image'); - } elseif ($element instanceof Object) { - $this->writeUnsupportedElement($xmlWriter, 'Object'); - } elseif ($element instanceof TOC) { - $this->writeUnsupportedElement($xmlWriter, 'TOC'); - } else { - $this->writeUnsupportedElement($xmlWriter, 'Element'); - } - } - } - } - $xmlWriter->endElement(); - $xmlWriter->endElement(); - $xmlWriter->endElement(); - - // Return - return $xmlWriter->getData(); - } - - /** - * Write text - * - * @param XMLWriter $xmlWriter - * @param Text $text - * @param bool $withoutP - */ - protected function writeText(XMLWriter $xmlWriter, Text $text, $withoutP = false) - { - $styleFont = $text->getFontStyle(); - $styleParagraph = $text->getParagraphStyle(); - - // @todo Commented for TextRun. Should really checkout this value - // $SfIsObject = ($styleFont instanceof Font) ? true : false; - $SfIsObject = false; - - if ($SfIsObject) { - // Don't never be the case, because I browse all sections for cleaning all styles not declared - die('PhpWord : $SfIsObject wouldn\'t be an object'); - } else { - if (!$withoutP) { - $xmlWriter->startElement('text:p'); // text:p - } - if (empty($styleFont)) { - if (empty($styleParagraph)) { - $xmlWriter->writeAttribute('text:style-name', 'P1'); - } elseif (is_string($styleParagraph)) { - $xmlWriter->writeAttribute('text:style-name', $styleParagraph); - } - $xmlWriter->writeRaw($text->getText()); - } else { - if (empty($styleParagraph)) { - $xmlWriter->writeAttribute('text:style-name', 'Standard'); - } elseif (is_string($styleParagraph)) { - $xmlWriter->writeAttribute('text:style-name', $styleParagraph); - } - // text:span - $xmlWriter->startElement('text:span'); - if (is_string($styleFont)) { - $xmlWriter->writeAttribute('text:style-name', $styleFont); - } - $xmlWriter->writeRaw($text->getText()); - $xmlWriter->endElement(); - } - if (!$withoutP) { - $xmlWriter->endElement(); // text:p - } - } - } - - /** - * Write TextRun section - * - * @param XMLWriter $xmlWriter - * @param TextRun $textrun - * @todo Enable all other section types - */ - protected function writeTextRun(XMLWriter $xmlWriter, TextRun $textrun) - { - $elements = $textrun->getElements(); - $xmlWriter->startElement('text:p'); - if (count($elements) > 0) { - foreach ($elements as $element) { - if ($element instanceof Text) { - $this->writeText($xmlWriter, $element, true); - } - } - } - $xmlWriter->endElement(); - } - - /** - * Write TextBreak - * - * @param XMLWriter $xmlWriter - */ - protected function writeTextBreak(XMLWriter $xmlWriter) - { - $xmlWriter->startElement('text:p'); - $xmlWriter->writeAttribute('text:style-name', 'Standard'); - $xmlWriter->endElement(); - } - - /** - * Write unsupported element - * - * @param XMLWriter $xmlWriter - * @param string $element - */ - private function writeUnsupportedElement($xmlWriter, $element) - { - $xmlWriter->startElement('text:p'); - $xmlWriter->writeRaw($element); - $xmlWriter->endElement(); } }