From 05f7973b743f1a3b15beb76adb9e2536e053b830 Mon Sep 17 00:00:00 2001 From: Progi1984 Date: Thu, 21 Sep 2023 20:10:24 +0200 Subject: [PATCH] Formula : Add Element (& Writer/Reader Word2007/ODText) --- composer.json | 3 +- composer.lock | 55 ++++++++++++- src/PhpWord/Element/AbstractContainer.php | 3 + src/PhpWord/Element/Formula.php | 53 +++++++++++++ src/PhpWord/Reader/ODText.php | 4 +- src/PhpWord/Reader/ODText/Content.php | 68 ++++++++++------ src/PhpWord/Reader/Word2007/AbstractPart.php | 74 ++++++++++-------- src/PhpWord/Writer/ODText.php | 26 ++++++ src/PhpWord/Writer/ODText/Element/Formula.php | 68 ++++++++++++++++ src/PhpWord/Writer/ODText/Element/Table.php | 1 + src/PhpWord/Writer/ODText/Element/TextRun.php | 1 + src/PhpWord/Writer/ODText/Element/Title.php | 1 + src/PhpWord/Writer/ODText/Part/Content.php | 3 + src/PhpWord/Writer/ODText/Part/Manifest.php | 18 ++++- .../Word2007/Element/AbstractElement.php | 18 +++++ .../Writer/Word2007/Element/Container.php | 2 + src/PhpWord/Writer/Word2007/Element/Field.php | 2 + .../Writer/Word2007/Element/Formula.php | 53 +++++++++++++ .../Writer/Word2007/Element/ListItemRun.php | 1 + src/PhpWord/Writer/Word2007/Element/Table.php | 1 + .../Writer/Word2007/Element/TextBox.php | 1 + .../Writer/Word2007/Element/TextRun.php | 1 + src/PhpWord/Writer/Word2007/Element/Title.php | 1 + src/PhpWord/Writer/Word2007/Part/Comments.php | 1 + src/PhpWord/Writer/Word2007/Part/Document.php | 1 + src/PhpWord/Writer/Word2007/Part/Footer.php | 1 + .../Writer/Word2007/Part/Footnotes.php | 1 + tests/PhpWordTests/Reader/ODTextTest.php | 33 +++++++- tests/PhpWordTests/Reader/Word2007Test.php | 57 ++++++++++++++ .../Writer/ODText/Element/FormulaTest.php | 72 +++++++++++++++++ .../Writer/Word2007/Element/FormulaTest.php | 72 +++++++++++++++++ .../_files/documents/reader-formula.docx | Bin 0 -> 4292 bytes .../_files/documents/reader-formula.odt | Bin 0 -> 10565 bytes 33 files changed, 628 insertions(+), 68 deletions(-) create mode 100644 src/PhpWord/Element/Formula.php create mode 100644 src/PhpWord/Writer/ODText/Element/Formula.php create mode 100644 src/PhpWord/Writer/Word2007/Element/Formula.php create mode 100644 tests/PhpWordTests/Writer/ODText/Element/FormulaTest.php create mode 100644 tests/PhpWordTests/Writer/Word2007/Element/FormulaTest.php create mode 100644 tests/PhpWordTests/_files/documents/reader-formula.docx create mode 100644 tests/PhpWordTests/_files/documents/reader-formula.odt diff --git a/composer.json b/composer.json index 6890bf6d9f..303e0556da 100644 --- a/composer.json +++ b/composer.json @@ -68,7 +68,8 @@ "ext-dom": "*", "ext-json": "*", "ext-xml": "*", - "laminas/laminas-escaper": ">=2.6" + "laminas/laminas-escaper": ">=2.6", + "phpoffice/math": "dev-master" }, "require-dev": { "ext-zip": "*", diff --git a/composer.lock b/composer.lock index a4557e01da..8b450b63e6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2d45739d1122eb36186b5cfe7cd6d6ab", + "content-hash": "ae95020f2e191f3202f8e1c5ac26b4e6", "packages": [ { "name": "laminas/laminas-escaper", @@ -67,6 +67,58 @@ } ], "time": "2022-10-10T10:11:09+00:00" + }, + { + "name": "phpoffice/math", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/Math.git", + "reference": "6cb7422c7165529e1f7f9ed0b00fad947d937224" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/6cb7422c7165529e1f7f9ed0b00fad947d937224", + "reference": "6cb7422c7165529e1f7f9ed0b00fad947d937224", + "shasum": "" + }, + "require": { + "ext-xml": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpunit/phpunit": ">=7.0" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\Math\\": "src/Math/", + "Tests\\PhpOffice\\Math\\": "tests/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Progi1984", + "homepage": "https://lefevre.dev" + } + ], + "description": "Math - Manipulate Math Formula", + "keywords": [ + "MathML", + "officemathml", + "php" + ], + "support": { + "issues": "https://github.com/PHPOffice/Math/issues", + "source": "https://github.com/PHPOffice/Math/tree/master" + }, + "time": "2023-09-21T16:13:12+00:00" } ], "packages-dev": [ @@ -5017,6 +5069,7 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { + "phpoffice/math": 20, "phpstan/phpstan-phpunit": 0 }, "prefer-stable": false, diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index 884ec29385..f9b2822a12 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord\Element; use BadMethodCallException; +use PhpOffice\Math\Math; use ReflectionClass; /** @@ -47,6 +48,7 @@ * @method Chart addChart(string $type, array $categories, array $values, array $style = null, $seriesName = null) * @method FormField addFormField(string $type, mixed $fStyle = null, mixed $pStyle = null) * @method SDT addSDT(string $type) + * @method Formula addFormula(Math $math) * @method \PhpOffice\PhpWord\Element\OLEObject addObject(string $source, mixed $style = null) deprecated, use addOLEObject instead * * @since 0.10.0 @@ -88,6 +90,7 @@ public function __call($function, $args) 'Footnote', 'Endnote', 'CheckBox', 'TextBox', 'Field', 'Line', 'Shape', 'Title', 'TOC', 'PageBreak', 'Chart', 'FormField', 'SDT', 'Comment', + 'Formula', ]; $functions = []; foreach ($elements as $element) { diff --git a/src/PhpWord/Element/Formula.php b/src/PhpWord/Element/Formula.php new file mode 100644 index 0000000000..ca9f5d6b4b --- /dev/null +++ b/src/PhpWord/Element/Formula.php @@ -0,0 +1,53 @@ +setMath($math); + } + + public function setMath(Math $math): self + { + $this->math = $math; + + return $this; + } + + public function getMath(): Math + { + return $this->math; + } +} diff --git a/src/PhpWord/Reader/ODText.php b/src/PhpWord/Reader/ODText.php index aba280db01..8973949cdc 100644 --- a/src/PhpWord/Reader/ODText.php +++ b/src/PhpWord/Reader/ODText.php @@ -59,7 +59,7 @@ public function load($docFile) * @param string $docFile * @param string $xmlFile */ - private function readPart(PhpWord $phpWord, $relationships, $partName, $docFile, $xmlFile): void + private function readPart(PhpWord $phpWord, array $relationships, string $partName, string $docFile, string $xmlFile): void { $partClass = "PhpOffice\\PhpWord\\Reader\\ODText\\{$partName}"; if (class_exists($partClass)) { @@ -77,7 +77,7 @@ private function readPart(PhpWord $phpWord, $relationships, $partName, $docFile, * * @return array */ - private function readRelationships($docFile) + private function readRelationships(string $docFile): array { $rels = []; $xmlFile = 'META-INF/manifest.xml'; diff --git a/src/PhpWord/Reader/ODText/Content.php b/src/PhpWord/Reader/ODText/Content.php index ccbc5eec96..a5b4a92ef6 100644 --- a/src/PhpWord/Reader/ODText/Content.php +++ b/src/PhpWord/Reader/ODText/Content.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord\Reader\ODText; use DateTime; +use PhpOffice\Math\Reader\MathML; use PhpOffice\PhpWord\Element\TrackChange; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Shared\XMLReader; @@ -51,37 +52,52 @@ public function read(PhpWord $phpWord): void break; case 'text:p': // Paragraph - $children = $node->childNodes; - foreach ($children as $child) { - switch ($child->nodeName) { - case 'text:change-start': - $changeId = $child->getAttribute('text:change-id'); - if (isset($trackedChanges[$changeId])) { - $changed = $trackedChanges[$changeId]; - } + $element = $xmlReader->getElement('draw:frame/draw:object', $node); + if ($element) { + $mathFile = str_replace('./', '', $element->getAttribute('xlink:href')) . '/content.xml'; - break; - case 'text:change-end': - unset($changed); + $xmlReaderObject = new XMLReader(); + $mathElement = $xmlReaderObject->getDomFromZip($this->docFile, $mathFile); - break; - case 'text:change': - $changeId = $child->getAttribute('text:change-id'); - if (isset($trackedChanges[$changeId])) { - $changed = $trackedChanges[$changeId]; - } + $mathXML = $mathElement->saveXML($mathElement); + + $reader = new MathML(); + $math = $reader->read($mathXML); + + $section->addFormula($math); + } else { + $children = $node->childNodes; + foreach ($children as $child) { + switch ($child->nodeName) { + case 'text:change-start': + $changeId = $child->getAttribute('text:change-id'); + if (isset($trackedChanges[$changeId])) { + $changed = $trackedChanges[$changeId]; + } + + break; + case 'text:change-end': + unset($changed); - break; + break; + case 'text:change': + $changeId = $child->getAttribute('text:change-id'); + if (isset($trackedChanges[$changeId])) { + $changed = $trackedChanges[$changeId]; + } + + break; + } } - } - $element = $section->addText($node->nodeValue); - if (isset($changed) && is_array($changed)) { - $element->setTrackChange($changed['changed']); - if (isset($changed['textNodes'])) { - foreach ($changed['textNodes'] as $changedNode) { - $element = $section->addText($changedNode->nodeValue); - $element->setTrackChange($changed['changed']); + $element = $section->addText($node->nodeValue); + if (isset($changed) && is_array($changed)) { + $element->setTrackChange($changed['changed']); + if (isset($changed['textNodes'])) { + foreach ($changed['textNodes'] as $changedNode) { + $element = $section->addText($changedNode->nodeValue); + $element->setTrackChange($changed['changed']); + } } } } diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index dc61b09356..b5cc79ba4a 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -20,6 +20,7 @@ use DateTime; use DOMElement; use InvalidArgumentException; +use PhpOffice\Math\Reader\OfficeMathML; use PhpOffice\PhpWord\ComplexType\TblWidth as TblWidthComplexType; use PhpOffice\PhpWord\Element\AbstractContainer; use PhpOffice\PhpWord\Element\AbstractElement; @@ -189,25 +190,7 @@ protected function getCommentReference(string $id): array protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart = 'document'): void { // Paragraph style - $paragraphStyle = null; - $headingDepth = null; - if ($xmlReader->elementExists('w:commentReference', $domNode) - || $xmlReader->elementExists('w:commentRangeStart', $domNode) - || $xmlReader->elementExists('w:commentRangeEnd', $domNode) - ) { - $nodes = $xmlReader->getElements('w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode); - $node = current(iterator_to_array($nodes)); - if ($node) { - $attributeIdentifier = $node->attributes->getNamedItem('id'); - if ($attributeIdentifier) { - $id = $attributeIdentifier->nodeValue; - } - } - } - if ($xmlReader->elementExists('w:pPr', $domNode)) { - $paragraphStyle = $this->readParagraphStyle($xmlReader, $domNode); - $headingDepth = $this->getHeadingDepth($paragraphStyle); - } + $paragraphStyle = $xmlReader->elementExists('w:pPr', $domNode) ? $this->readParagraphStyle($xmlReader, $domNode) : null; // PreserveText if ($xmlReader->elementExists('w:r/w:instrText', $domNode)) { @@ -234,8 +217,26 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par } } $parent->addPreserveText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'), $fontStyle, $paragraphStyle); - } elseif ($xmlReader->elementExists('w:pPr/w:numPr', $domNode)) { - // List item + + return; + } + + // Formula + $xmlReader->registerNamespace('m', 'http://schemas.openxmlformats.org/officeDocument/2006/math'); + if ($xmlReader->elementExists('m:oMath', $domNode)) { + $mathElement = $xmlReader->getElement('m:oMath', $domNode); + $mathXML = $mathElement->ownerDocument->saveXML($mathElement); + + $reader = new OfficeMathML(); + $math = $reader->read($mathXML); + + $parent->addFormula($math); + + return; + } + + // List item + if ($xmlReader->elementExists('w:pPr/w:numPr', $domNode)) { $numId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:numId'); $levelId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:ilvl'); $nodes = $xmlReader->getElements('*', $domNode); @@ -245,8 +246,13 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par foreach ($nodes as $node) { $this->readRun($xmlReader, $node, $listItemRun, $docPart, $paragraphStyle); } - } elseif ($headingDepth !== null) { - // Heading or Title + + return; + } + + // Heading or Title + $headingDepth = $xmlReader->elementExists('w:pPr', $domNode) ? $this->getHeadingDepth($paragraphStyle) : null; + if ($headingDepth !== null) { $textContent = null; $nodes = $xmlReader->getElements('w:r|w:hyperlink', $domNode); if ($nodes->length === 1) { @@ -258,17 +264,19 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par } } $parent->addTitle($textContent, $headingDepth); + + return; + } + + // Text and TextRun + $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode); + if (0 === $textRunContainers) { + $parent->addTextBreak(null, $paragraphStyle); } else { - // Text and TextRun - $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode); - if (0 === $textRunContainers) { - $parent->addTextBreak(null, $paragraphStyle); - } else { - $nodes = $xmlReader->getElements('*', $domNode); - $paragraph = $parent->addTextRun($paragraphStyle); - foreach ($nodes as $node) { - $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle); - } + $nodes = $xmlReader->getElements('*', $domNode); + $paragraph = $parent->addTextRun($paragraphStyle); + foreach ($nodes as $node) { + $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle); } } } diff --git a/src/PhpWord/Writer/ODText.php b/src/PhpWord/Writer/ODText.php index 10d9d701fa..df364b5acc 100644 --- a/src/PhpWord/Writer/ODText.php +++ b/src/PhpWord/Writer/ODText.php @@ -17,6 +17,8 @@ namespace PhpOffice\PhpWord\Writer; +use PhpOffice\Math\Writer\MathML; +use PhpOffice\PhpWord\Element\Formula; use PhpOffice\PhpWord\Media; use PhpOffice\PhpWord\PhpWord; @@ -27,6 +29,8 @@ */ class ODText extends AbstractWriter implements WriterInterface { + protected $objects = []; + /** * Create new ODText writer. * @@ -82,8 +86,30 @@ public function save($filename = null): void } } + // Write objects charts + if (!empty($this->objects)) { + $writer = new MathML(); + foreach ($this->objects as $idxObject => $object) { + if ($object instanceof Formula) { + $zip->addFromString('Formula'. $idxObject . '/content.xml', $writer->write($object->getMath())); + } + } + } + // Close zip archive and cleanup temp file $zip->close(); $this->cleanupTempFile(); } + + public function addObject(object $object): int + { + $this->objects[] = $object; + + return count($this->objects) - 1; + } + + public function getObjects(): array + { + return $this->objects; + } } diff --git a/src/PhpWord/Writer/ODText/Element/Formula.php b/src/PhpWord/Writer/ODText/Element/Formula.php new file mode 100644 index 0000000000..db4d3d8fd1 --- /dev/null +++ b/src/PhpWord/Writer/ODText/Element/Formula.php @@ -0,0 +1,68 @@ +getXmlWriter(); + $element = $this->getElement(); + if (!$element instanceof ElementFormula) { + return; + } + + $objectIdx = $this->getPart()->getParentWriter()->addObject($element); + + //$style = $element->getStyle(); + //$width = Converter::pixelToCm($style->getWidth()); + //$height = Converter::pixelToCm($style->getHeight()); + + $xmlWriter->startElement('text:p'); + $xmlWriter->writeAttribute('text:style-name', 'OB' . $objectIdx); + + $xmlWriter->startElement('draw:frame'); + $xmlWriter->writeAttribute('draw:name', $element->getElementId()); + $xmlWriter->writeAttribute('text:anchor-type', 'as-char'); + //$xmlWriter->writeAttribute('svg:width', $width . 'cm'); + //$xmlWriter->writeAttribute('svg:height', $height . 'cm'); + //$xmlWriter->writeAttribute('draw:z-index', $mediaIndex); + + $xmlWriter->startElement('draw:object'); + $xmlWriter->writeAttribute('xlink:href', 'Formula' . $objectIdx); + $xmlWriter->writeAttribute('xlink:type', 'simple'); + $xmlWriter->writeAttribute('xlink:show', 'embed'); + $xmlWriter->writeAttribute('xlink:actuate', 'onLoad'); + $xmlWriter->endElement(); // draw:object + + $xmlWriter->endElement(); // draw:frame + + $xmlWriter->endElement(); // text:p + } +} diff --git a/src/PhpWord/Writer/ODText/Element/Table.php b/src/PhpWord/Writer/ODText/Element/Table.php index 783bb21232..951226fff3 100644 --- a/src/PhpWord/Writer/ODText/Element/Table.php +++ b/src/PhpWord/Writer/ODText/Element/Table.php @@ -83,6 +83,7 @@ private function writeRow(XMLWriter $xmlWriter, RowElement $row): void $xmlWriter->writeAttribute('office:value-type', 'string'); $containerWriter = new Container($xmlWriter, $cell); + $containerWriter->setPart($this->getPart()); $containerWriter->write(); $xmlWriter->endElement(); // table:table-cell diff --git a/src/PhpWord/Writer/ODText/Element/TextRun.php b/src/PhpWord/Writer/ODText/Element/TextRun.php index 6d1e1a191a..28e9f80eb7 100644 --- a/src/PhpWord/Writer/ODText/Element/TextRun.php +++ b/src/PhpWord/Writer/ODText/Element/TextRun.php @@ -41,6 +41,7 @@ public function write(): void $xmlWriter->writeAttribute('text:style-name', $pStyle); $containerWriter = new Container($xmlWriter, $element); + $containerWriter->setPart($this->getPart()); $containerWriter->write(); $xmlWriter->endElement(); diff --git a/src/PhpWord/Writer/ODText/Element/Title.php b/src/PhpWord/Writer/ODText/Element/Title.php index 45fe65c7f5..7730a64e4c 100644 --- a/src/PhpWord/Writer/ODText/Element/Title.php +++ b/src/PhpWord/Writer/ODText/Element/Title.php @@ -57,6 +57,7 @@ public function write(): void $this->writeText($text); } elseif ($text instanceof \PhpOffice\PhpWord\Element\AbstractContainer) { $containerWriter = new Container($xmlWriter, $text); + $containerWriter->setPart($this->getPart()); $containerWriter->write(); } $xmlWriter->endElement(); // text:span diff --git a/src/PhpWord/Writer/ODText/Part/Content.php b/src/PhpWord/Writer/ODText/Part/Content.php index 8c96650240..825d36e3b8 100644 --- a/src/PhpWord/Writer/ODText/Part/Content.php +++ b/src/PhpWord/Writer/ODText/Part/Content.php @@ -135,8 +135,11 @@ public function write() $xmlWriter->startElement('text:p'); $xmlWriter->writeAttribute('text:style-name', 'SB' . $section->getSectionId()); $xmlWriter->endElement(); + $containerWriter = new Container($xmlWriter, $section); + $containerWriter->setPart($this); $containerWriter->write(); + $xmlWriter->endElement(); // text:section } diff --git a/src/PhpWord/Writer/ODText/Part/Manifest.php b/src/PhpWord/Writer/ODText/Part/Manifest.php index 7d428b2c76..fb4f6a6f2b 100644 --- a/src/PhpWord/Writer/ODText/Part/Manifest.php +++ b/src/PhpWord/Writer/ODText/Part/Manifest.php @@ -17,6 +17,7 @@ namespace PhpOffice\PhpWord\Writer\ODText\Part; +use PhpOffice\PhpWord\Element\Formula; use PhpOffice\PhpWord\Media; /** @@ -31,7 +32,6 @@ class Manifest extends AbstractPart */ public function write() { - $parts = ['content.xml', 'meta.xml', 'styles.xml']; $xmlWriter = $this->getXmlWriter(); $xmlWriter->startDocument('1.0', 'UTF-8'); @@ -46,7 +46,7 @@ public function write() $xmlWriter->endElement(); // Parts - foreach ($parts as $part) { + foreach (['content.xml', 'meta.xml', 'styles.xml'] as $part) { $xmlWriter->startElement('manifest:file-entry'); $xmlWriter->writeAttribute('manifest:media-type', 'text/xml'); $xmlWriter->writeAttribute('manifest:full-path', $part); @@ -64,6 +64,20 @@ public function write() } } + foreach($this->getParentWriter()->getObjects() as $idxObject => $object) { + if ($object instanceof Formula) { + $xmlWriter->startElement('manifest:file-entry'); + $xmlWriter->writeAttribute('manifest:full-path', 'Formula' . $idxObject. '/content.xml'); + $xmlWriter->writeAttribute('manifest:media-type', 'text/xml'); + $xmlWriter->endElement(); + $xmlWriter->startElement('manifest:file-entry'); + $xmlWriter->writeAttribute('manifest:full-path', 'Formula' . $idxObject. '/'); + $xmlWriter->writeAttribute('manifest:version', '1.2'); + $xmlWriter->writeAttribute('manifest:media-type', 'application/vnd.oasis.opendocument.formula'); + $xmlWriter->endElement(); + } + } + $xmlWriter->endElement(); // manifest:manifest return $xmlWriter->getData(); diff --git a/src/PhpWord/Writer/Word2007/Element/AbstractElement.php b/src/PhpWord/Writer/Word2007/Element/AbstractElement.php index a7bfeab78b..5851f915aa 100644 --- a/src/PhpWord/Writer/Word2007/Element/AbstractElement.php +++ b/src/PhpWord/Writer/Word2007/Element/AbstractElement.php @@ -21,6 +21,7 @@ use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Shared\Text as SharedText; use PhpOffice\PhpWord\Shared\XMLWriter; +use PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart; /** * Abstract element writer. @@ -50,6 +51,11 @@ abstract class AbstractElement */ protected $withoutP = false; + /** + * @var AbstractPart|null + */ + protected $part; + /** * Write element. */ @@ -224,4 +230,16 @@ protected function writeText($content) return $this->getXmlWriter()->writeRaw($content); } + + public function setPart(?AbstractPart $part): self + { + $this->part = $part; + + return $this; + } + + public function getPart(): ?AbstractPart + { + return $this->part; + } } diff --git a/src/PhpWord/Writer/Word2007/Element/Container.php b/src/PhpWord/Writer/Word2007/Element/Container.php index b6db45197a..88a46cd0c9 100644 --- a/src/PhpWord/Writer/Word2007/Element/Container.php +++ b/src/PhpWord/Writer/Word2007/Element/Container.php @@ -64,6 +64,7 @@ public function write(): void $writerClass = $this->namespace . '\\TextBreak'; /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $writer Type hint */ $writer = new $writerClass($xmlWriter, new TextBreakElement(), $withoutP); + $writer->setPart($this->getPart()); $writer->write(); } } @@ -83,6 +84,7 @@ private function writeElement(XMLWriter $xmlWriter, Element $element, $withoutP) if (class_exists($writerClass)) { /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $writer Type hint */ $writer = new $writerClass($xmlWriter, $element, $withoutP); + $writer->setPart($this->getPart()); $writer->write(); } diff --git a/src/PhpWord/Writer/Word2007/Element/Field.php b/src/PhpWord/Writer/Word2007/Element/Field.php index 6fdb48b0f4..d889651ac2 100644 --- a/src/PhpWord/Writer/Word2007/Element/Field.php +++ b/src/PhpWord/Writer/Word2007/Element/Field.php @@ -75,6 +75,7 @@ private function writeDefault(\PhpOffice\PhpWord\Element\Field $element): void if ($element->getText() != null) { if ($element->getText() instanceof \PhpOffice\PhpWord\Element\TextRun) { $containerWriter = new Container($xmlWriter, $element->getText(), true); + $containerWriter->setPart($this->getPart()); $containerWriter->write(); $xmlWriter->startElement('w:r'); @@ -146,6 +147,7 @@ protected function writeMacrobutton(\PhpOffice\PhpWord\Element\Field $element): if ($element->getText() != null) { if ($element->getText() instanceof \PhpOffice\PhpWord\Element\TextRun) { $containerWriter = new Container($xmlWriter, $element->getText(), true); + $containerWriter->setPart($this->getPart()); $containerWriter->write(); } } diff --git a/src/PhpWord/Writer/Word2007/Element/Formula.php b/src/PhpWord/Writer/Word2007/Element/Formula.php new file mode 100644 index 0000000000..00286cb7dd --- /dev/null +++ b/src/PhpWord/Writer/Word2007/Element/Formula.php @@ -0,0 +1,53 @@ +getXmlWriter(); + $element = $this->getElement(); + if (!$element instanceof FormulaElement) { + return; + } + + $this->startElementP(); + + $xmlWriter->startElement('w:r'); + $xmlWriter->writeElement('w:rPr'); + $xmlWriter->endElement(); + + $writer = new OfficeMathML(); + + $xmlWriter->writeRaw( + $writer->write($element->getMath()) + ); + + $this->endElementP(); + } +} diff --git a/src/PhpWord/Writer/Word2007/Element/ListItemRun.php b/src/PhpWord/Writer/Word2007/Element/ListItemRun.php index daa2fc1d0f..2d86b3abac 100644 --- a/src/PhpWord/Writer/Word2007/Element/ListItemRun.php +++ b/src/PhpWord/Writer/Word2007/Element/ListItemRun.php @@ -49,6 +49,7 @@ private function writeParagraph(ListItemRunElement $element): void $this->writeParagraphProperties($element); $containerWriter = new Container($xmlWriter, $element); + $containerWriter->setPart($this->getPart()); $containerWriter->write(); $xmlWriter->endElement(); // w:p diff --git a/src/PhpWord/Writer/Word2007/Element/Table.php b/src/PhpWord/Writer/Word2007/Element/Table.php index 9364fe45c1..a47b86d04c 100644 --- a/src/PhpWord/Writer/Word2007/Element/Table.php +++ b/src/PhpWord/Writer/Word2007/Element/Table.php @@ -127,6 +127,7 @@ private function writeCell(XMLWriter $xmlWriter, CellElement $cell): void // Write content $containerWriter = new Container($xmlWriter, $cell); + $containerWriter->setPart($this->getPart()); $containerWriter->write(); $xmlWriter->endElement(); // w:tc diff --git a/src/PhpWord/Writer/Word2007/Element/TextBox.php b/src/PhpWord/Writer/Word2007/Element/TextBox.php index ff94094de7..9f02c215e7 100644 --- a/src/PhpWord/Writer/Word2007/Element/TextBox.php +++ b/src/PhpWord/Writer/Word2007/Element/TextBox.php @@ -61,6 +61,7 @@ public function write(): void // TextBox content, serving as a container $xmlWriter->startElement('w:txbxContent'); $containerWriter = new Container($xmlWriter, $element); + $containerWriter->setPart($this->getPart()); $containerWriter->write(); $xmlWriter->endElement(); // w:txbxContent diff --git a/src/PhpWord/Writer/Word2007/Element/TextRun.php b/src/PhpWord/Writer/Word2007/Element/TextRun.php index 8a5870777b..b631ac4dd5 100644 --- a/src/PhpWord/Writer/Word2007/Element/TextRun.php +++ b/src/PhpWord/Writer/Word2007/Element/TextRun.php @@ -35,6 +35,7 @@ public function write(): void $this->startElementP(); $containerWriter = new Container($xmlWriter, $element); + $containerWriter->setPart($this->getPart()); $containerWriter->write(); $this->endElementP(); // w:p diff --git a/src/PhpWord/Writer/Word2007/Element/Title.php b/src/PhpWord/Writer/Word2007/Element/Title.php index dd46d755e3..a1a6351fee 100644 --- a/src/PhpWord/Writer/Word2007/Element/Title.php +++ b/src/PhpWord/Writer/Word2007/Element/Title.php @@ -69,6 +69,7 @@ public function write(): void $xmlWriter->endElement(); // w:r } elseif ($text instanceof \PhpOffice\PhpWord\Element\AbstractContainer) { $containerWriter = new Container($xmlWriter, $text); + $containerWriter->setPart($this->getPart()); $containerWriter->write(); } diff --git a/src/PhpWord/Writer/Word2007/Part/Comments.php b/src/PhpWord/Writer/Word2007/Part/Comments.php index 93dd4e1ce5..425c11534a 100644 --- a/src/PhpWord/Writer/Word2007/Part/Comments.php +++ b/src/PhpWord/Writer/Word2007/Part/Comments.php @@ -81,6 +81,7 @@ protected function writeComment(XMLWriter $xmlWriter, Comment $comment): void $xmlWriter->writeAttributeIf($comment->getInitials() != null, 'w:initials', $comment->getInitials()); $containerWriter = new Container($xmlWriter, $comment); + $containerWriter->setPart($this); $containerWriter->write(); $xmlWriter->endElement(); // w:comment diff --git a/src/PhpWord/Writer/Word2007/Part/Document.php b/src/PhpWord/Writer/Word2007/Part/Document.php index 6eca90e730..0b2740cac3 100644 --- a/src/PhpWord/Writer/Word2007/Part/Document.php +++ b/src/PhpWord/Writer/Word2007/Part/Document.php @@ -61,6 +61,7 @@ public function write() ++$currentSection; $containerWriter = new Container($xmlWriter, $section); + $containerWriter->setPart($this); $containerWriter->write(); if ($currentSection == $sectionCount) { diff --git a/src/PhpWord/Writer/Word2007/Part/Footer.php b/src/PhpWord/Writer/Word2007/Part/Footer.php index fd62c8941f..79de91c085 100644 --- a/src/PhpWord/Writer/Word2007/Part/Footer.php +++ b/src/PhpWord/Writer/Word2007/Part/Footer.php @@ -61,6 +61,7 @@ public function write() $xmlWriter->writeAttribute('xmlns:wne', 'http://schemas.microsoft.com/office/word/2006/wordml'); $containerWriter = new Container($xmlWriter, $this->element); + $containerWriter->setPart($this); $containerWriter->write(); $xmlWriter->endElement(); // $this->rootElement diff --git a/src/PhpWord/Writer/Word2007/Part/Footnotes.php b/src/PhpWord/Writer/Word2007/Part/Footnotes.php index 9624ab7459..349ea45c2b 100644 --- a/src/PhpWord/Writer/Word2007/Part/Footnotes.php +++ b/src/PhpWord/Writer/Word2007/Part/Footnotes.php @@ -168,6 +168,7 @@ protected function writeNote(XMLWriter $xmlWriter, $element): void $xmlWriter->endElement(); // w:r $containerWriter = new Container($xmlWriter, $element); + $containerWriter->setPart($this); $containerWriter->write(); $xmlWriter->endElement(); // w:p diff --git a/tests/PhpWordTests/Reader/ODTextTest.php b/tests/PhpWordTests/Reader/ODTextTest.php index e4baf8d480..20c5916c7d 100644 --- a/tests/PhpWordTests/Reader/ODTextTest.php +++ b/tests/PhpWordTests/Reader/ODTextTest.php @@ -17,7 +17,11 @@ namespace PhpOffice\PhpWordTests\Reader; +use PhpOffice\Math\Element; +use PhpOffice\PhpWord\Element\Formula; +use PhpOffice\PhpWord\Element\Section; use PhpOffice\PhpWord\IOFactory; +use PhpOffice\PhpWord\PhpWord; /** * Test class for PhpOffice\PhpWord\Reader\ODText. @@ -33,8 +37,31 @@ class ODTextTest extends \PHPUnit\Framework\TestCase */ public function testLoad(): void { - $filename = __DIR__ . '/../_files/documents/reader.odt'; - $phpWord = IOFactory::load($filename, 'ODText'); - self::assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $phpWord); + $phpWord = IOFactory::load(dirname(__DIR__, 1) . '/_files/documents/reader.odt', 'ODText'); + self::assertInstanceOf(PhpWord::class, $phpWord); + } + + public function testLoadFormula(): void + { + $phpWord = IOFactory::load(dirname(__DIR__, 1) . '/_files/documents/reader-formula.odt', 'ODText'); + + self::assertInstanceOf(PhpWord::class, $phpWord); + + $sections = $phpWord->getSections(); + self::assertCount(1, $sections); + + $section = $sections[0]; + self::assertInstanceOf(Section::class, $section); + + $elements = $section->getElements(); + self::assertCount(1, $elements); + + $element = $elements[0]; + self::assertInstanceOf(Formula::class, $element); + + $elements = $element->getMath()->getElements(); + self::assertCount(1, $elements); + + self::assertInstanceOf(Element\Semantics::class, $elements[0]); } } diff --git a/tests/PhpWordTests/Reader/Word2007Test.php b/tests/PhpWordTests/Reader/Word2007Test.php index e42f0110d5..1d8674d729 100644 --- a/tests/PhpWordTests/Reader/Word2007Test.php +++ b/tests/PhpWordTests/Reader/Word2007Test.php @@ -18,8 +18,11 @@ namespace PhpOffice\PhpWordTests\Reader; use DateTime; +use PhpOffice\Math\Element; use PhpOffice\PhpWord\Element\Comment; +use PhpOffice\PhpWord\Element\Formula; use PhpOffice\PhpWord\Element\Image; +use PhpOffice\PhpWord\Element\Section; use PhpOffice\PhpWord\Element\Text; use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\IOFactory; @@ -159,4 +162,58 @@ public function testLoadComments(): void self::assertInstanceOf(Font::class, $fontStyle); self::assertEquals('de-DE', $fontStyle->getLang()->getLatin()); } + + public function testLoadFormula(): void + { + $phpWord = IOFactory::load(dirname(__DIR__, 1) . '/_files/documents/reader-formula.docx'); + + self::assertInstanceOf(PhpWord::class, $phpWord); + + $sections = $phpWord->getSections(); + self::assertCount(1, $sections); + + $section = $sections[0]; + self::assertInstanceOf(Section::class, $section); + + $elements = $section->getElements(); + self::assertCount(1, $elements); + + $element = $elements[0]; + self::assertInstanceOf(Formula::class, $element); + + $elements = $element->getMath()->getElements(); + self::assertCount(5, $elements); + + /** @var Element\Fraction $element */ + $element = $elements[0]; + self::assertInstanceOf(Element\Fraction::class, $element); + /** @var Element\Identifier $numerator */ + $numerator = $element->getNumerator(); + self::assertInstanceOf(Element\Identifier::class, $numerator); + self::assertEquals('π', $numerator->getValue()); + /** @var Element\Numeric $denominator */ + $denominator = $element->getDenominator(); + self::assertInstanceOf(Element\Numeric::class, $denominator); + self::assertEquals(2, $denominator->getValue()); + + /** @var Element\Operator $element */ + $element = $elements[1]; + self::assertInstanceOf(Element\Operator::class, $element); + self::assertEquals('+', $element->getValue()); + + /** @var Element\Identifier $element */ + $element = $elements[2]; + self::assertInstanceOf(Element\Identifier::class, $element); + self::assertEquals('a', $element->getValue()); + + /** @var Element\Operator $element */ + $element = $elements[3]; + self::assertInstanceOf(Element\Operator::class, $element); + self::assertEquals('∗', $element->getValue()); + + /** @var Element\Numeric $element */ + $element = $elements[4]; + self::assertInstanceOf(Element\Numeric::class, $element); + self::assertEquals(2, $element->getValue()); + } } diff --git a/tests/PhpWordTests/Writer/ODText/Element/FormulaTest.php b/tests/PhpWordTests/Writer/ODText/Element/FormulaTest.php new file mode 100644 index 0000000000..45229404d8 --- /dev/null +++ b/tests/PhpWordTests/Writer/ODText/Element/FormulaTest.php @@ -0,0 +1,72 @@ +add( + (new Element\Fraction()) + ->setDenominator(new Element\Identifier('π')) + ->setNumerator(new Element\Numeric(2)) + ) + ->add( + new Element\Operator('+') + ) + ->add( + new Element\Identifier('a') + ) + ->add( + new Element\Operator('∗') + ) + ->add( + new Element\Numeric('2') + ) + ; + + $phpWord = new PhpWord(); + + $section = $phpWord->addSection(); + $section->addFormula($math); + + $doc = TestHelperDOCX::getDocument($phpWord, 'ODText'); + + self::assertTrue($doc->elementExists('/office:document-content/office:body/office:text/text:section/text:p/draw:frame/draw:object')); + //self::assertTrue($doc->elementExists('/w:document/w:body/w:p[1]/m:oMathPara/m:oMath')); + } +} diff --git a/tests/PhpWordTests/Writer/Word2007/Element/FormulaTest.php b/tests/PhpWordTests/Writer/Word2007/Element/FormulaTest.php new file mode 100644 index 0000000000..23a5710283 --- /dev/null +++ b/tests/PhpWordTests/Writer/Word2007/Element/FormulaTest.php @@ -0,0 +1,72 @@ +add( + (new Element\Fraction()) + ->setDenominator(new Element\Identifier('π')) + ->setNumerator(new Element\Numeric(2)) + ) + ->add( + new Element\Operator('+') + ) + ->add( + new Element\Identifier('a') + ) + ->add( + new Element\Operator('∗') + ) + ->add( + new Element\Numeric('2') + ) + ; + + $phpWord = new PhpWord(); + + $section = $phpWord->addSection(); + $section->addFormula($math); + + $doc = TestHelperDOCX::getDocument($phpWord); + + self::assertTrue($doc->elementExists('/w:document/w:body/w:p[1]/m:oMathPara')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:p[1]/m:oMathPara/m:oMath')); + } +} diff --git a/tests/PhpWordTests/_files/documents/reader-formula.docx b/tests/PhpWordTests/_files/documents/reader-formula.docx new file mode 100644 index 0000000000000000000000000000000000000000..0e40f6672c93bc6860e6f531e0ad216c67c85816 GIT binary patch literal 4292 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 Hq+kC5`84qb literal 0 HcmV?d00001 diff --git a/tests/PhpWordTests/_files/documents/reader-formula.odt b/tests/PhpWordTests/_files/documents/reader-formula.odt new file mode 100644 index 0000000000000000000000000000000000000000..f94c44afbb50a140896952bb05e63862adb53934 GIT binary patch literal 10565 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 literal 0 HcmV?d00001