From 5e147c4ffeb87a7188e377ec89ff55501bea5d50 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:23:05 -0800 Subject: [PATCH 1/9] WIP Do Not Install All my unimplemented changes. Not intended for install. --- phpstan-baseline.neon | 5 - samples/Sample_45_RTLTitles.php | 35 ++ src/PhpWord/PhpWord.php | 5 +- src/PhpWord/Reader/Word2007/AbstractPart.php | 65 ++- src/PhpWord/Reader/Word2007/Styles.php | 6 +- src/PhpWord/Settings.php | 5 + src/PhpWord/Shared/Html.php | 209 +++++-- src/PhpWord/Shared/HtmlColours.php | 549 ++++++++++++++++++ .../Shared/Microsoft/PasswordEncoder.php | 11 +- src/PhpWord/Shared/ZipArchive.php | 10 +- src/PhpWord/SimpleType/TextDirection.php | 55 ++ src/PhpWord/Style.php | 10 +- src/PhpWord/Style/AbstractStyle.php | 15 + src/PhpWord/Style/Border.php | 8 + src/PhpWord/Style/Paragraph.php | 48 +- src/PhpWord/Style/Table.php | 113 ++++ src/PhpWord/TemplateProcessor.php | 12 +- src/PhpWord/Writer/HTML/Element/Table.php | 5 +- src/PhpWord/Writer/HTML/Element/Title.php | 13 +- src/PhpWord/Writer/HTML/Part/Head.php | 10 +- src/PhpWord/Writer/HTML/Style/Font.php | 7 + src/PhpWord/Writer/HTML/Style/Table.php | 6 +- src/PhpWord/Writer/RTF.php | 2 +- src/PhpWord/Writer/RTF/Element/Title.php | 9 +- src/PhpWord/Writer/Word2007/Element/Table.php | 10 +- src/PhpWord/Writer/Word2007/Part/Styles.php | 17 +- src/PhpWord/Writer/Word2007/Style/Font.php | 5 +- .../Writer/Word2007/Style/MarginBorder.php | 18 +- .../Writer/Word2007/Style/Paragraph.php | 5 + src/PhpWord/Writer/Word2007/Style/Table.php | 3 + .../Reader/Word2007/StyleTableTest.php | 55 ++ tests/PhpWordTests/SettingsRtlTest.php | 81 +++ tests/PhpWordTests/SettingsTest.php | 16 - tests/PhpWordTests/Shared/Html2402Test.php | 208 +++++++ tests/PhpWordTests/Shared/HtmlFullTest.php | 93 +++ .../PhpWordTests/Shared/HtmlHeadingsTest.php | 66 +++ tests/PhpWordTests/Shared/HtmlRtlTest.php | 180 ++++++ tests/PhpWordTests/Shared/HtmlTest.php | 39 +- .../TemplateProcessorSectionTest.php | 92 +++ tests/PhpWordTests/TemplateProcessorTest.php | 10 + tests/PhpWordTests/Writer/HTML/FontTest.php | 56 +- tests/PhpWordTests/Writer/HTML/Helper.php | 11 +- tests/PhpWordTests/Writer/HTML/PartTest.php | 10 +- .../Writer/ODText/Part/ContentTest.php | 10 + .../Writer/ODText/Style/Paragraph2Test.php | 6 +- .../Writer/RTF/RichTextTitleTest.php | 50 ++ .../Writer/Word2007/Element/TableTest.php | 147 +++++ .../_files/documents/word.2474.docx | Bin 0 -> 27593 bytes 48 files changed, 2190 insertions(+), 211 deletions(-) create mode 100644 samples/Sample_45_RTLTitles.php create mode 100644 src/PhpWord/Shared/HtmlColours.php create mode 100644 src/PhpWord/SimpleType/TextDirection.php create mode 100644 tests/PhpWordTests/Reader/Word2007/StyleTableTest.php create mode 100644 tests/PhpWordTests/SettingsRtlTest.php create mode 100644 tests/PhpWordTests/Shared/Html2402Test.php create mode 100644 tests/PhpWordTests/Shared/HtmlFullTest.php create mode 100644 tests/PhpWordTests/Shared/HtmlHeadingsTest.php create mode 100644 tests/PhpWordTests/Shared/HtmlRtlTest.php create mode 100644 tests/PhpWordTests/TemplateProcessorSectionTest.php create mode 100644 tests/PhpWordTests/Writer/RTF/RichTextTitleTest.php create mode 100644 tests/PhpWordTests/Writer/Word2007/Element/TableTest.php create mode 100644 tests/PhpWordTests/_files/documents/word.2474.docx diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2e44745b3d..e07918f6b6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -400,11 +400,6 @@ parameters: count: 1 path: src/PhpWord/Shared/Html.php - - - message: "#^Cannot call method setBorderSize\\(\\) on PhpOffice\\\\PhpWord\\\\Style\\\\Table\\|string\\.$#" - count: 1 - path: src/PhpWord/Shared/Html.php - - message: "#^Cannot call method setStyleName\\(\\) on PhpOffice\\\\PhpWord\\\\Style\\\\Table\\|string\\.$#" count: 1 diff --git a/samples/Sample_45_RTLTitles.php b/samples/Sample_45_RTLTitles.php new file mode 100644 index 0000000000..83dd9b9872 --- /dev/null +++ b/samples/Sample_45_RTLTitles.php @@ -0,0 +1,35 @@ +setDefaultFontName('DejaVu Sans'); // for good rendition of PDF +$rendererName = Settings::PDF_RENDERER_MPDF; +$rendererLibraryPath = $vendorDirPath . '/mpdf/mpdf'; +Settings::setPdfRenderer($rendererName, $rendererLibraryPath); + +// Define styles for headers +$phpWord->addTitleStyle(1, ['bold' => true, 'name' => 'Arial', 'size' => 16], []); +//var_dump($x); +$phpWord->addTitleStyle(2, ['bold' => true, 'name' => 'Arial', 'size' => 14], []); +$phpWord->addTitleStyle(3, ['bold' => true, 'name' => 'Arial', 'size' => 12], []); +$phpWord->addTitleStyle(4, ['bold' => true, 'name' => 'Arial', 'size' => 10], []); + +// New section +$section = $phpWord->addSection(); +$htmlContent = '
مرحبا here كلمة انجليزي.
'; +SharedHtml::addHtml($section, $htmlContent, false, false); + +// Save file +echo write($phpWord, basename(__FILE__, '.php'), $writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} +Settings::setDefaultRtl(false); diff --git a/src/PhpWord/PhpWord.php b/src/PhpWord/PhpWord.php index a7aa95ce45..c85306e67c 100644 --- a/src/PhpWord/PhpWord.php +++ b/src/PhpWord/PhpWord.php @@ -20,6 +20,7 @@ use BadMethodCallException; use PhpOffice\PhpWord\Element\Section; use PhpOffice\PhpWord\Exception\Exception; +use PhpOffice\PhpWord\Style\Font; /** * PHPWord main class. @@ -284,9 +285,9 @@ public function setDefaultFontSize($fontSize): void * * @return \PhpOffice\PhpWord\Style\Paragraph */ - public function setDefaultParagraphStyle($styles) + public function setDefaultParagraphStyle($styles, ?Font $fontStyles = null) { - return Style::setDefaultParagraphStyle($styles); + return Style::setDefaultParagraphStyle($styles, $fontStyles); } /** diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index 95799387ed..a92e6d5958 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -592,35 +592,46 @@ protected function readTableStyle(XMLReader $xmlReader, DOMElement $domNode) $borders = array_merge($margins, ['insideH', 'insideV']); if ($xmlReader->elementExists('w:tblPr', $domNode)) { + $tblStyleName = ''; if ($xmlReader->elementExists('w:tblPr/w:tblStyle', $domNode)) { - $style = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle'); - } else { - $styleNode = $xmlReader->getElement('w:tblPr', $domNode); - $styleDefs = []; - foreach ($margins as $side) { - $ucfSide = ucfirst($side); - $styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w']; - } - foreach ($borders as $side) { - $ucfSide = ucfirst($side); - $styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz']; - $styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color']; - $styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val']; - } - $styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type']; - $styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual']; - $styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w']; - $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs); - - $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode); - if ($tablePositionNode !== null) { - $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode); - } + $tblStyleName = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle'); + } + $styleNode = $xmlReader->getElement('w:tblPr', $domNode); + $styleDefs = []; - $indentNode = $xmlReader->getElement('w:tblInd', $styleNode); - if ($indentNode !== null) { - $style['indent'] = $this->readTableIndent($xmlReader, $indentNode); - } + foreach ($margins as $side) { + $ucfSide = ucfirst($side); + $styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w']; + } + foreach ($borders as $side) { + $ucfSide = ucfirst($side); + $styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz']; + $styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color']; + $styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val']; + } + $styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type']; + $styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual']; + $styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w']; + $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs); + + $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode); + if ($tablePositionNode !== null) { + $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode); + } + + $indentNode = $xmlReader->getElement('w:tblInd', $styleNode); + if ($indentNode !== null) { + $style['indent'] = $this->readTableIndent($xmlReader, $indentNode); + } + if ($xmlReader->elementExists('w:basedOn', $domNode)) { + $style['basedOn'] = $xmlReader->getAttribute('w:val', $domNode, 'w:basedOn'); + } + if ($tblStyleName !== '') { + $style['tblStyle'] = $tblStyleName; + } + // this may be unneeded + if ($xmlReader->elementExists('w:name', $domNode)) { + $style['styleName'] = $xmlReader->getAttribute('w:val', $domNode, 'w:name'); } } diff --git a/src/PhpWord/Reader/Word2007/Styles.php b/src/PhpWord/Reader/Word2007/Styles.php index 760adf9493..f67bc77463 100644 --- a/src/PhpWord/Reader/Word2007/Styles.php +++ b/src/PhpWord/Reader/Word2007/Styles.php @@ -65,8 +65,9 @@ public function read(PhpWord $phpWord): void foreach ($nodes as $node) { $type = $xmlReader->getAttribute('w:type', $node); $name = $xmlReader->getAttribute('w:val', $node, 'w:name'); + $styleId = $xmlReader->getAttribute('w:styleId', $node); if (null === $name) { - $name = $xmlReader->getAttribute('w:styleId', $node); + $name = $styleId; } $headingMatches = []; preg_match('/Heading\s*(\d)/i', $name, $headingMatches); @@ -98,7 +99,8 @@ public function read(PhpWord $phpWord): void case 'table': $tStyle = $this->readTableStyle($xmlReader, $node); if (!empty($tStyle)) { - $phpWord->addTableStyle($name, $tStyle); + $newTable = $phpWord->addTableStyle($styleId, $tStyle); + $newTable->setStyleName($name); } break; diff --git a/src/PhpWord/Settings.php b/src/PhpWord/Settings.php index 984486ccfe..b43bf05228 100644 --- a/src/PhpWord/Settings.php +++ b/src/PhpWord/Settings.php @@ -15,6 +15,8 @@ namespace PhpOffice\PhpWord; +use PhpOffice\PhpWord\SimpleType\TextDirection; + /** * PHPWord settings class. * @@ -397,6 +399,9 @@ public static function setDefaultFontSize($value): bool public static function setDefaultRtl(?bool $defaultRtl): void { self::$defaultRtl = $defaultRtl; + if ($defaultRtl === true && Style::getStyle('Normal') === null) { + Style::setDefaultParagraphStyle(['bidi' => true, 'textDirection' => TextDirection::RLTB], ['rtl' => true]); + } } public static function isDefaultRtl(): ?bool diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 2022f7da09..21d8404ddc 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -25,9 +25,14 @@ use PhpOffice\PhpWord\Element\AbstractContainer; use PhpOffice\PhpWord\Element\Row; use PhpOffice\PhpWord\Element\Table; +use PhpOffice\PhpWord\Element\TextRun; +use PhpOffice\PhpWord\Metadata\DocInfo; +use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Settings; +use PhpOffice\PhpWord\SimpleType\Border; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\NumberFormat; +use PhpOffice\PhpWord\SimpleType\TextDirection; use PhpOffice\PhpWord\Style\Paragraph; /** @@ -37,6 +42,8 @@ */ class Html { + private const SPECIAL_BORDER_WIDTHS = ['thin' => '0.5pt', 'thick' => '3.5pt', 'medium' => '2.0pt']; + private const RGB_REGEXP = '/^\s*rgb\s*[(]\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*[)]\s*$/'; protected static $listIndex = 0; @@ -45,6 +52,9 @@ class Html protected static $options; + /** @var ?DocInfo */ + protected static $docInfo; + /** * @var Css */ @@ -69,6 +79,14 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit * which could be applied when such an element occurs in the parseNode function. */ static::$options = $options; + static::$docInfo = null; + if (method_exists($element, 'getPhpWord')) { + /** @var ?PhpWord */ + $phpWord = $element->getPhpWord(); + if ($phpWord !== null) { + static::$docInfo = $phpWord->getDocInfo(); + } + } // Preprocess: remove all line ends, decode HTML entity, // fix ampersand and angle brackets and add body tag for HTML fragments @@ -84,17 +102,20 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit // Load DOM if (\PHP_VERSION_ID < 80000) { - $orignalLibEntityLoader = libxml_disable_entity_loader(true); + $orignalLibEntityLoader = libxml_disable_entity_loader(true); // @codeCoverageIgnore } $dom = new DOMDocument(); $dom->preserveWhiteSpace = $preserveWhiteSpace; $dom->loadXML($html); static::$xpath = new DOMXPath($dom); - $node = $dom->getElementsByTagName('body'); + $node = $dom->getElementsByTagName('html'); + if (count($node) === 0) { + $node = $dom->getElementsByTagName('body'); + } static::parseNode($node->item(0), $element); if (\PHP_VERSION_ID < 80000) { - libxml_disable_entity_loader($orignalLibEntityLoader); + libxml_disable_entity_loader($orignalLibEntityLoader); // @codeCoverageIgnore } } @@ -106,12 +127,20 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit * * @return array */ - protected static function parseInlineStyle($node, $styles = []) + protected static function parseInlineStyle($node, &$styles) { if (XML_ELEMENT_NODE == $node->nodeType) { $attributes = $node->attributes; // get all the attributes(eg: id, class) - $bidi = ($attributes['dir'] ?? '') === 'rtl'; + $bidi = false; + $direction = isset($attributes['dir']) ? $attributes['dir']->value : ''; + if ($direction === 'rtl') { + $bidi = $styles['bidi'] = $styles['rtl'] = true; + $styles['textDirection'] = TextDirection::RLTB; + } elseif ($direction === 'ltr') { + $bidi = $styles['bidi'] = $styles['rtl'] = false; + $styles['textDirection'] = TextDirection::LRTB; + } foreach ($attributes as $attribute) { $val = $attribute->value; switch (strtolower($attribute->name)) { @@ -144,7 +173,7 @@ protected static function parseInlineStyle($node, $styles = []) break; case 'bgcolor': // tables, rows, cells e.g.header a | +header b | +header c | +
---|---|---|
1 | 2 | |
This is bold text | 6 |
header a | +header b | +header c | +
---|---|---|
1 | 2 | |
This is bold text | 6 |