AbstractPart.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. <?php
  2. /**
  3. * This file is part of PHPWord - A pure PHP library for reading and writing
  4. * word processing documents.
  5. *
  6. * PHPWord is free software distributed under the terms of the GNU Lesser
  7. * General Public License version 3 as published by the Free Software Foundation.
  8. *
  9. * For the full copyright and license information, please read the LICENSE
  10. * file that was distributed with this source code. For the full list of
  11. * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
  12. *
  13. * @see https://github.com/PHPOffice/PHPWord
  14. * @copyright 2010-2018 PHPWord contributors
  15. * @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
  16. */
  17. namespace PhpOffice\PhpWord\Reader\Word2007;
  18. use PhpOffice\PhpWord\ComplexType\TblWidth as TblWidthComplexType;
  19. use PhpOffice\PhpWord\Element\AbstractContainer;
  20. use PhpOffice\PhpWord\Element\TextRun;
  21. use PhpOffice\PhpWord\Element\TrackChange;
  22. use PhpOffice\PhpWord\PhpWord;
  23. use PhpOffice\PhpWord\Shared\XMLReader;
  24. /**
  25. * Abstract part reader
  26. *
  27. * This class is inherited by ODText reader
  28. *
  29. * @since 0.10.0
  30. */
  31. abstract class AbstractPart
  32. {
  33. /**
  34. * Conversion method
  35. *
  36. * @const int
  37. */
  38. const READ_VALUE = 'attributeValue'; // Read attribute value
  39. const READ_EQUAL = 'attributeEquals'; // Read `true` when attribute value equals specified value
  40. const READ_TRUE = 'attributeTrue'; // Read `true` when element exists
  41. const READ_FALSE = 'attributeFalse'; // Read `false` when element exists
  42. const READ_SIZE = 'attributeMultiplyByTwo'; // Read special attribute value for Font::$size
  43. /**
  44. * Document file
  45. *
  46. * @var string
  47. */
  48. protected $docFile;
  49. /**
  50. * XML file
  51. *
  52. * @var string
  53. */
  54. protected $xmlFile;
  55. /**
  56. * Part relationships
  57. *
  58. * @var array
  59. */
  60. protected $rels = array();
  61. /**
  62. * Read part.
  63. */
  64. abstract public function read(PhpWord $phpWord);
  65. /**
  66. * Create new instance
  67. *
  68. * @param string $docFile
  69. * @param string $xmlFile
  70. */
  71. public function __construct($docFile, $xmlFile)
  72. {
  73. $this->docFile = $docFile;
  74. $this->xmlFile = $xmlFile;
  75. }
  76. /**
  77. * Set relationships.
  78. *
  79. * @param array $value
  80. */
  81. public function setRels($value)
  82. {
  83. $this->rels = $value;
  84. }
  85. /**
  86. * Read w:p.
  87. *
  88. * @param \PhpOffice\PhpWord\Shared\XMLReader $xmlReader
  89. * @param \DOMElement $domNode
  90. * @param \PhpOffice\PhpWord\Element\AbstractContainer $parent
  91. * @param string $docPart
  92. *
  93. * @todo Get font style for preserve text
  94. */
  95. protected function readParagraph(XMLReader $xmlReader, \DOMElement $domNode, $parent, $docPart = 'document')
  96. {
  97. // Paragraph style
  98. $paragraphStyle = null;
  99. $headingDepth = null;
  100. if ($xmlReader->elementExists('w:pPr', $domNode)) {
  101. $paragraphStyle = $this->readParagraphStyle($xmlReader, $domNode);
  102. $headingDepth = $this->getHeadingDepth($paragraphStyle);
  103. }
  104. // PreserveText
  105. if ($xmlReader->elementExists('w:r/w:instrText', $domNode)) {
  106. $ignoreText = false;
  107. $textContent = '';
  108. $fontStyle = $this->readFontStyle($xmlReader, $domNode);
  109. $nodes = $xmlReader->getElements('w:r', $domNode);
  110. foreach ($nodes as $node) {
  111. $instrText = $xmlReader->getValue('w:instrText', $node);
  112. if ($xmlReader->elementExists('w:fldChar', $node)) {
  113. $fldCharType = $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar');
  114. if ('begin' == $fldCharType) {
  115. $ignoreText = true;
  116. } elseif ('end' == $fldCharType) {
  117. $ignoreText = false;
  118. }
  119. }
  120. if (!is_null($instrText)) {
  121. $textContent .= '{' . $instrText . '}';
  122. } else {
  123. if (false === $ignoreText) {
  124. $textContent .= $xmlReader->getValue('w:t', $node);
  125. }
  126. }
  127. }
  128. $parent->addPreserveText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'), $fontStyle, $paragraphStyle);
  129. } elseif ($xmlReader->elementExists('w:pPr/w:numPr', $domNode)) {
  130. // List item
  131. $numId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:numId');
  132. $levelId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:ilvl');
  133. $nodes = $xmlReader->getElements('*', $domNode);
  134. $listItemRun = $parent->addListItemRun($levelId, "PHPWordList{$numId}", $paragraphStyle);
  135. foreach ($nodes as $node) {
  136. $this->readRun($xmlReader, $node, $listItemRun, $docPart, $paragraphStyle);
  137. }
  138. } elseif ($headingDepth !== null) {
  139. // Heading or Title
  140. $textContent = null;
  141. $nodes = $xmlReader->getElements('w:r', $domNode);
  142. if ($nodes->length === 1) {
  143. $textContent = htmlspecialchars($xmlReader->getValue('w:t', $nodes->item(0)), ENT_QUOTES, 'UTF-8');
  144. } else {
  145. $textContent = new TextRun($paragraphStyle);
  146. foreach ($nodes as $node) {
  147. $this->readRun($xmlReader, $node, $textContent, $docPart, $paragraphStyle);
  148. }
  149. }
  150. $parent->addTitle($textContent, $headingDepth);
  151. } else {
  152. // Text and TextRun
  153. $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag', $domNode);
  154. if (0 === $textRunContainers) {
  155. $parent->addTextBreak(null, $paragraphStyle);
  156. } else {
  157. $nodes = $xmlReader->getElements('*', $domNode);
  158. $paragraph = $parent->addTextRun($paragraphStyle);
  159. foreach ($nodes as $node) {
  160. $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle);
  161. }
  162. }
  163. }
  164. }
  165. /**
  166. * Returns the depth of the Heading, returns 0 for a Title
  167. *
  168. * @param array $paragraphStyle
  169. * @return number|null
  170. */
  171. private function getHeadingDepth(array $paragraphStyle = null)
  172. {
  173. if (is_array($paragraphStyle) && isset($paragraphStyle['styleName'])) {
  174. if ('Title' === $paragraphStyle['styleName']) {
  175. return 0;
  176. }
  177. $headingMatches = array();
  178. preg_match('/Heading(\d)/', $paragraphStyle['styleName'], $headingMatches);
  179. if (!empty($headingMatches)) {
  180. return $headingMatches[1];
  181. }
  182. }
  183. return null;
  184. }
  185. /**
  186. * Read w:r.
  187. *
  188. * @param \PhpOffice\PhpWord\Shared\XMLReader $xmlReader
  189. * @param \DOMElement $domNode
  190. * @param \PhpOffice\PhpWord\Element\AbstractContainer $parent
  191. * @param string $docPart
  192. * @param mixed $paragraphStyle
  193. *
  194. * @todo Footnote paragraph style
  195. */
  196. protected function readRun(XMLReader $xmlReader, \DOMElement $domNode, $parent, $docPart, $paragraphStyle = null)
  197. {
  198. if (in_array($domNode->nodeName, array('w:ins', 'w:del', 'w:smartTag', 'w:hyperlink'))) {
  199. $nodes = $xmlReader->getElements('*', $domNode);
  200. foreach ($nodes as $node) {
  201. $this->readRun($xmlReader, $node, $parent, $docPart, $paragraphStyle);
  202. }
  203. } elseif ($domNode->nodeName == 'w:r') {
  204. $fontStyle = $this->readFontStyle($xmlReader, $domNode);
  205. $nodes = $xmlReader->getElements('*', $domNode);
  206. foreach ($nodes as $node) {
  207. $this->readRunChild($xmlReader, $node, $parent, $docPart, $paragraphStyle, $fontStyle);
  208. }
  209. }
  210. }
  211. /**
  212. * Parses nodes under w:r
  213. *
  214. * @param XMLReader $xmlReader
  215. * @param \DOMElement $node
  216. * @param AbstractContainer $parent
  217. * @param string $docPart
  218. * @param mixed $paragraphStyle
  219. * @param mixed $fontStyle
  220. */
  221. protected function readRunChild(XMLReader $xmlReader, \DOMElement $node, AbstractContainer $parent, $docPart, $paragraphStyle = null, $fontStyle = null)
  222. {
  223. $runParent = $node->parentNode->parentNode;
  224. if ($node->nodeName == 'w:footnoteReference') {
  225. // Footnote
  226. $wId = $xmlReader->getAttribute('w:id', $node);
  227. $footnote = $parent->addFootnote();
  228. $footnote->setRelationId($wId);
  229. } elseif ($node->nodeName == 'w:endnoteReference') {
  230. // Endnote
  231. $wId = $xmlReader->getAttribute('w:id', $node);
  232. $endnote = $parent->addEndnote();
  233. $endnote->setRelationId($wId);
  234. } elseif ($node->nodeName == 'w:pict') {
  235. // Image
  236. $rId = $xmlReader->getAttribute('r:id', $node, 'v:shape/v:imagedata');
  237. $target = $this->getMediaTarget($docPart, $rId);
  238. if (!is_null($target)) {
  239. if ('External' == $this->getTargetMode($docPart, $rId)) {
  240. $imageSource = $target;
  241. } else {
  242. $imageSource = "zip://{$this->docFile}#{$target}";
  243. }
  244. $parent->addImage($imageSource);
  245. }
  246. } elseif ($node->nodeName == 'w:drawing') {
  247. // Office 2011 Image
  248. $xmlReader->registerNamespace('wp', 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing');
  249. $xmlReader->registerNamespace('r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
  250. $xmlReader->registerNamespace('pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture');
  251. $xmlReader->registerNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
  252. $name = $xmlReader->getAttribute('name', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr');
  253. $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip');
  254. if ($name === null && $embedId === null) { // some Converters puts images on a different path
  255. $name = $xmlReader->getAttribute('name', $node, 'wp:anchor/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr');
  256. $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:anchor/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip');
  257. }
  258. $target = $this->getMediaTarget($docPart, $embedId);
  259. if (!is_null($target)) {
  260. $imageSource = "zip://{$this->docFile}#{$target}";
  261. $parent->addImage($imageSource, null, false, $name);
  262. }
  263. } elseif ($node->nodeName == 'w:object') {
  264. // Object
  265. $rId = $xmlReader->getAttribute('r:id', $node, 'o:OLEObject');
  266. // $rIdIcon = $xmlReader->getAttribute('r:id', $domNode, 'w:object/v:shape/v:imagedata');
  267. $target = $this->getMediaTarget($docPart, $rId);
  268. if (!is_null($target)) {
  269. $textContent = "&lt;Object: {$target}>";
  270. $parent->addText($textContent, $fontStyle, $paragraphStyle);
  271. }
  272. } elseif ($node->nodeName == 'w:br') {
  273. $parent->addTextBreak();
  274. } elseif ($node->nodeName == 'w:tab') {
  275. $parent->addText("\t");
  276. } elseif ($node->nodeName == 'mc:AlternateContent') {
  277. if ($node->hasChildNodes()) {
  278. // Get fallback instead of mc:Choice to make sure it is compatible
  279. $fallbackElements = $node->getElementsByTagName('Fallback');
  280. if ($fallbackElements->length) {
  281. $fallback = $fallbackElements->item(0);
  282. // TextRun
  283. $textContent = htmlspecialchars($fallback->nodeValue, ENT_QUOTES, 'UTF-8');
  284. $parent->addText($textContent, $fontStyle, $paragraphStyle);
  285. }
  286. }
  287. } elseif ($node->nodeName == 'w:t' || $node->nodeName == 'w:delText') {
  288. // TextRun
  289. $textContent = htmlspecialchars($xmlReader->getValue('.', $node), ENT_QUOTES, 'UTF-8');
  290. if ($runParent->nodeName == 'w:hyperlink') {
  291. $rId = $xmlReader->getAttribute('r:id', $runParent);
  292. $target = $this->getMediaTarget($docPart, $rId);
  293. if (!is_null($target)) {
  294. $parent->addLink($target, $textContent, $fontStyle, $paragraphStyle);
  295. } else {
  296. $parent->addText($textContent, $fontStyle, $paragraphStyle);
  297. }
  298. } else {
  299. /** @var AbstractElement $element */
  300. $element = $parent->addText($textContent, $fontStyle, $paragraphStyle);
  301. if (in_array($runParent->nodeName, array('w:ins', 'w:del'))) {
  302. $type = ($runParent->nodeName == 'w:del') ? TrackChange::DELETED : TrackChange::INSERTED;
  303. $author = $runParent->getAttribute('w:author');
  304. $date = \DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $runParent->getAttribute('w:date'));
  305. $element->setChangeInfo($type, $author, $date);
  306. }
  307. }
  308. }
  309. }
  310. /**
  311. * Read w:tbl.
  312. *
  313. * @param \PhpOffice\PhpWord\Shared\XMLReader $xmlReader
  314. * @param \DOMElement $domNode
  315. * @param mixed $parent
  316. * @param string $docPart
  317. */
  318. protected function readTable(XMLReader $xmlReader, \DOMElement $domNode, $parent, $docPart = 'document')
  319. {
  320. // Table style
  321. $tblStyle = null;
  322. if ($xmlReader->elementExists('w:tblPr', $domNode)) {
  323. $tblStyle = $this->readTableStyle($xmlReader, $domNode);
  324. }
  325. /** @var \PhpOffice\PhpWord\Element\Table $table Type hint */
  326. $table = $parent->addTable($tblStyle);
  327. $tblNodes = $xmlReader->getElements('*', $domNode);
  328. foreach ($tblNodes as $tblNode) {
  329. if ('w:tblGrid' == $tblNode->nodeName) { // Column
  330. // @todo Do something with table columns
  331. } elseif ('w:tr' == $tblNode->nodeName) { // Row
  332. $rowHeight = $xmlReader->getAttribute('w:val', $tblNode, 'w:trPr/w:trHeight');
  333. $rowHRule = $xmlReader->getAttribute('w:hRule', $tblNode, 'w:trPr/w:trHeight');
  334. $rowHRule = $rowHRule == 'exact';
  335. $rowStyle = array(
  336. 'tblHeader' => $xmlReader->elementExists('w:trPr/w:tblHeader', $tblNode),
  337. 'cantSplit' => $xmlReader->elementExists('w:trPr/w:cantSplit', $tblNode),
  338. 'exactHeight' => $rowHRule,
  339. );
  340. $row = $table->addRow($rowHeight, $rowStyle);
  341. $rowNodes = $xmlReader->getElements('*', $tblNode);
  342. foreach ($rowNodes as $rowNode) {
  343. if ('w:trPr' == $rowNode->nodeName) { // Row style
  344. // @todo Do something with row style
  345. } elseif ('w:tc' == $rowNode->nodeName) { // Cell
  346. $cellWidth = $xmlReader->getAttribute('w:w', $rowNode, 'w:tcPr/w:tcW');
  347. $cellStyle = null;
  348. $cellStyleNode = $xmlReader->getElement('w:tcPr', $rowNode);
  349. if (!is_null($cellStyleNode)) {
  350. $cellStyle = $this->readCellStyle($xmlReader, $cellStyleNode);
  351. }
  352. $cell = $row->addCell($cellWidth, $cellStyle);
  353. $cellNodes = $xmlReader->getElements('*', $rowNode);
  354. foreach ($cellNodes as $cellNode) {
  355. if ('w:p' == $cellNode->nodeName) { // Paragraph
  356. $this->readParagraph($xmlReader, $cellNode, $cell, $docPart);
  357. }
  358. }
  359. }
  360. }
  361. }
  362. }
  363. }
  364. /**
  365. * Read w:pPr.
  366. *
  367. * @param \PhpOffice\PhpWord\Shared\XMLReader $xmlReader
  368. * @param \DOMElement $domNode
  369. * @return array|null
  370. */
  371. protected function readParagraphStyle(XMLReader $xmlReader, \DOMElement $domNode)
  372. {
  373. if (!$xmlReader->elementExists('w:pPr', $domNode)) {
  374. return null;
  375. }
  376. $styleNode = $xmlReader->getElement('w:pPr', $domNode);
  377. $styleDefs = array(
  378. 'styleName' => array(self::READ_VALUE, array('w:pStyle', 'w:name')),
  379. 'alignment' => array(self::READ_VALUE, 'w:jc'),
  380. 'basedOn' => array(self::READ_VALUE, 'w:basedOn'),
  381. 'next' => array(self::READ_VALUE, 'w:next'),
  382. 'indent' => array(self::READ_VALUE, 'w:ind', 'w:left'),
  383. 'hanging' => array(self::READ_VALUE, 'w:ind', 'w:hanging'),
  384. 'spaceAfter' => array(self::READ_VALUE, 'w:spacing', 'w:after'),
  385. 'spaceBefore' => array(self::READ_VALUE, 'w:spacing', 'w:before'),
  386. 'widowControl' => array(self::READ_FALSE, 'w:widowControl'),
  387. 'keepNext' => array(self::READ_TRUE, 'w:keepNext'),
  388. 'keepLines' => array(self::READ_TRUE, 'w:keepLines'),
  389. 'pageBreakBefore' => array(self::READ_TRUE, 'w:pageBreakBefore'),
  390. 'contextualSpacing' => array(self::READ_TRUE, 'w:contextualSpacing'),
  391. 'bidi' => array(self::READ_TRUE, 'w:bidi'),
  392. 'suppressAutoHyphens' => array(self::READ_TRUE, 'w:suppressAutoHyphens'),
  393. );
  394. return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
  395. }
  396. /**
  397. * Read w:rPr
  398. *
  399. * @param \PhpOffice\PhpWord\Shared\XMLReader $xmlReader
  400. * @param \DOMElement $domNode
  401. * @return array|null
  402. */
  403. protected function readFontStyle(XMLReader $xmlReader, \DOMElement $domNode)
  404. {
  405. if (is_null($domNode)) {
  406. return null;
  407. }
  408. // Hyperlink has an extra w:r child
  409. if ('w:hyperlink' == $domNode->nodeName) {
  410. $domNode = $xmlReader->getElement('w:r', $domNode);
  411. }
  412. if (!$xmlReader->elementExists('w:rPr', $domNode)) {
  413. return null;
  414. }
  415. $styleNode = $xmlReader->getElement('w:rPr', $domNode);
  416. $styleDefs = array(
  417. 'styleName' => array(self::READ_VALUE, 'w:rStyle'),
  418. 'name' => array(self::READ_VALUE, 'w:rFonts', array('w:ascii', 'w:hAnsi', 'w:eastAsia', 'w:cs')),
  419. 'hint' => array(self::READ_VALUE, 'w:rFonts', 'w:hint'),
  420. 'size' => array(self::READ_SIZE, array('w:sz', 'w:szCs')),
  421. 'color' => array(self::READ_VALUE, 'w:color'),
  422. 'underline' => array(self::READ_VALUE, 'w:u'),
  423. 'bold' => array(self::READ_TRUE, 'w:b'),
  424. 'italic' => array(self::READ_TRUE, 'w:i'),
  425. 'strikethrough' => array(self::READ_TRUE, 'w:strike'),
  426. 'doubleStrikethrough' => array(self::READ_TRUE, 'w:dstrike'),
  427. 'smallCaps' => array(self::READ_TRUE, 'w:smallCaps'),
  428. 'allCaps' => array(self::READ_TRUE, 'w:caps'),
  429. 'superScript' => array(self::READ_EQUAL, 'w:vertAlign', 'w:val', 'superscript'),
  430. 'subScript' => array(self::READ_EQUAL, 'w:vertAlign', 'w:val', 'subscript'),
  431. 'fgColor' => array(self::READ_VALUE, 'w:highlight'),
  432. 'rtl' => array(self::READ_TRUE, 'w:rtl'),
  433. 'lang' => array(self::READ_VALUE, 'w:lang'),
  434. 'position' => array(self::READ_VALUE, 'w:position'),
  435. 'hidden' => array(self::READ_TRUE, 'w:vanish'),
  436. );
  437. return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
  438. }
  439. /**
  440. * Read w:tblPr
  441. *
  442. * @param \PhpOffice\PhpWord\Shared\XMLReader $xmlReader
  443. * @param \DOMElement $domNode
  444. * @return string|array|null
  445. * @todo Capture w:tblStylePr w:type="firstRow"
  446. */
  447. protected function readTableStyle(XMLReader $xmlReader, \DOMElement $domNode)
  448. {
  449. $style = null;
  450. $margins = array('top', 'left', 'bottom', 'right');
  451. $borders = array_merge($margins, array('insideH', 'insideV'));
  452. if ($xmlReader->elementExists('w:tblPr', $domNode)) {
  453. if ($xmlReader->elementExists('w:tblPr/w:tblStyle', $domNode)) {
  454. $style = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle');
  455. } else {
  456. $styleNode = $xmlReader->getElement('w:tblPr', $domNode);
  457. $styleDefs = array();
  458. foreach ($margins as $side) {
  459. $ucfSide = ucfirst($side);
  460. $styleDefs["cellMargin$ucfSide"] = array(self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w');
  461. }
  462. foreach ($borders as $side) {
  463. $ucfSide = ucfirst($side);
  464. $styleDefs["border{$ucfSide}Size"] = array(self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz');
  465. $styleDefs["border{$ucfSide}Color"] = array(self::READ_VALUE, "w:tblBorders/w:$side", 'w:color');
  466. $styleDefs["border{$ucfSide}Style"] = array(self::READ_VALUE, "w:tblBorders/w:$side", 'w:val');
  467. }
  468. $styleDefs['layout'] = array(self::READ_VALUE, 'w:tblLayout', 'w:type');
  469. $styleDefs['bidiVisual'] = array(self::READ_TRUE, 'w:bidiVisual');
  470. $styleDefs['cellSpacing'] = array(self::READ_VALUE, 'w:tblCellSpacing', 'w:w');
  471. $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
  472. $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode);
  473. if ($tablePositionNode !== null) {
  474. $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode);
  475. }
  476. $indentNode = $xmlReader->getElement('w:tblInd', $styleNode);
  477. if ($indentNode !== null) {
  478. $style['indent'] = $this->readTableIndent($xmlReader, $indentNode);
  479. }
  480. }
  481. }
  482. return $style;
  483. }
  484. /**
  485. * Read w:tblpPr
  486. *
  487. * @param \PhpOffice\PhpWord\Shared\XMLReader $xmlReader
  488. * @param \DOMElement $domNode
  489. * @return array
  490. */
  491. private function readTablePosition(XMLReader $xmlReader, \DOMElement $domNode)
  492. {
  493. $styleDefs = array(
  494. 'leftFromText' => array(self::READ_VALUE, '.', 'w:leftFromText'),
  495. 'rightFromText' => array(self::READ_VALUE, '.', 'w:rightFromText'),
  496. 'topFromText' => array(self::READ_VALUE, '.', 'w:topFromText'),
  497. 'bottomFromText' => array(self::READ_VALUE, '.', 'w:bottomFromText'),
  498. 'vertAnchor' => array(self::READ_VALUE, '.', 'w:vertAnchor'),
  499. 'horzAnchor' => array(self::READ_VALUE, '.', 'w:horzAnchor'),
  500. 'tblpXSpec' => array(self::READ_VALUE, '.', 'w:tblpXSpec'),
  501. 'tblpX' => array(self::READ_VALUE, '.', 'w:tblpX'),
  502. 'tblpYSpec' => array(self::READ_VALUE, '.', 'w:tblpYSpec'),
  503. 'tblpY' => array(self::READ_VALUE, '.', 'w:tblpY'),
  504. );
  505. return $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
  506. }
  507. /**
  508. * Read w:tblInd
  509. *
  510. * @param \PhpOffice\PhpWord\Shared\XMLReader $xmlReader
  511. * @param \DOMElement $domNode
  512. * @return TblWidthComplexType
  513. */
  514. private function readTableIndent(XMLReader $xmlReader, \DOMElement $domNode)
  515. {
  516. $styleDefs = array(
  517. 'value' => array(self::READ_VALUE, '.', 'w:w'),
  518. 'type' => array(self::READ_VALUE, '.', 'w:type'),
  519. );
  520. $styleDefs = $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
  521. return new TblWidthComplexType((int) $styleDefs['value'], $styleDefs['type']);
  522. }
  523. /**
  524. * Read w:tcPr
  525. *
  526. * @param \PhpOffice\PhpWord\Shared\XMLReader $xmlReader
  527. * @param \DOMElement $domNode
  528. * @return array
  529. */
  530. private function readCellStyle(XMLReader $xmlReader, \DOMElement $domNode)
  531. {
  532. $styleDefs = array(
  533. 'valign' => array(self::READ_VALUE, 'w:vAlign'),
  534. 'textDirection' => array(self::READ_VALUE, 'w:textDirection'),
  535. 'gridSpan' => array(self::READ_VALUE, 'w:gridSpan'),
  536. 'vMerge' => array(self::READ_VALUE, 'w:vMerge'),
  537. 'bgColor' => array(self::READ_VALUE, 'w:shd', 'w:fill'),
  538. );
  539. return $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
  540. }
  541. /**
  542. * Returns the first child element found
  543. *
  544. * @param XMLReader $xmlReader
  545. * @param \DOMElement|null $parentNode
  546. * @param string|array|null $elements
  547. * @return string|null
  548. */
  549. private function findPossibleElement(XMLReader $xmlReader, \DOMElement $parentNode = null, $elements = null)
  550. {
  551. if (is_array($elements)) {
  552. //if element is an array, we take the first element that exists in the XML
  553. foreach ($elements as $possibleElement) {
  554. if ($xmlReader->elementExists($possibleElement, $parentNode)) {
  555. return $possibleElement;
  556. }
  557. }
  558. } else {
  559. return $elements;
  560. }
  561. return null;
  562. }
  563. /**
  564. * Returns the first attribute found
  565. *
  566. * @param XMLReader $xmlReader
  567. * @param \DOMElement $node
  568. * @param string|array $attributes
  569. * @return string|null
  570. */
  571. private function findPossibleAttribute(XMLReader $xmlReader, \DOMElement $node, $attributes)
  572. {
  573. //if attribute is an array, we take the first attribute that exists in the XML
  574. if (is_array($attributes)) {
  575. foreach ($attributes as $possibleAttribute) {
  576. if ($xmlReader->getAttribute($possibleAttribute, $node)) {
  577. return $possibleAttribute;
  578. }
  579. }
  580. return null;
  581. }
  582. return $attributes;
  583. }
  584. /**
  585. * Read style definition
  586. *
  587. * @param \PhpOffice\PhpWord\Shared\XMLReader $xmlReader
  588. * @param \DOMElement $parentNode
  589. * @param array $styleDefs
  590. * @ignoreScrutinizerPatch
  591. * @return array
  592. */
  593. protected function readStyleDefs(XMLReader $xmlReader, \DOMElement $parentNode = null, $styleDefs = array())
  594. {
  595. $styles = array();
  596. foreach ($styleDefs as $styleProp => $styleVal) {
  597. list($method, $element, $attribute, $expected) = array_pad($styleVal, 4, null);
  598. $element = $this->findPossibleElement($xmlReader, $parentNode, $element);
  599. if ($element === null) {
  600. continue;
  601. }
  602. if ($xmlReader->elementExists($element, $parentNode)) {
  603. $node = $xmlReader->getElement($element, $parentNode);
  604. $attribute = $this->findPossibleAttribute($xmlReader, $node, $attribute);
  605. // Use w:val as default if no attribute assigned
  606. $attribute = ($attribute === null) ? 'w:val' : $attribute;
  607. $attributeValue = $xmlReader->getAttribute($attribute, $node);
  608. $styleValue = $this->readStyleDef($method, $attributeValue, $expected);
  609. if ($styleValue !== null) {
  610. $styles[$styleProp] = $styleValue;
  611. }
  612. }
  613. }
  614. return $styles;
  615. }
  616. /**
  617. * Return style definition based on conversion method
  618. *
  619. * @param string $method
  620. * @ignoreScrutinizerPatch
  621. * @param string|null $attributeValue
  622. * @param mixed $expected
  623. * @return mixed
  624. */
  625. private function readStyleDef($method, $attributeValue, $expected)
  626. {
  627. $style = $attributeValue;
  628. if (self::READ_SIZE == $method) {
  629. $style = $attributeValue / 2;
  630. } elseif (self::READ_TRUE == $method) {
  631. $style = $this->isOn($attributeValue);
  632. } elseif (self::READ_FALSE == $method) {
  633. $style = !$this->isOn($attributeValue);
  634. } elseif (self::READ_EQUAL == $method) {
  635. $style = $attributeValue == $expected;
  636. }
  637. return $style;
  638. }
  639. /**
  640. * Parses the value of the on/off value, null is considered true as it means the w:val attribute was not present
  641. *
  642. * @see http://www.datypic.com/sc/ooxml/t-w_ST_OnOff.html
  643. * @param string $value
  644. * @return bool
  645. */
  646. private function isOn($value = null)
  647. {
  648. return $value === null || $value === '1' || $value === 'true' || $value === 'on';
  649. }
  650. /**
  651. * Returns the target of image, object, or link as stored in ::readMainRels
  652. *
  653. * @param string $docPart
  654. * @param string $rId
  655. * @return string|null
  656. */
  657. private function getMediaTarget($docPart, $rId)
  658. {
  659. $target = null;
  660. if (isset($this->rels[$docPart]) && isset($this->rels[$docPart][$rId])) {
  661. $target = $this->rels[$docPart][$rId]['target'];
  662. }
  663. return $target;
  664. }
  665. /**
  666. * Returns the target mode
  667. *
  668. * @param string $docPart
  669. * @param string $rId
  670. * @return string|null
  671. */
  672. private function getTargetMode($docPart, $rId)
  673. {
  674. $mode = null;
  675. if (isset($this->rels[$docPart]) && isset($this->rels[$docPart][$rId])) {
  676. $mode = $this->rels[$docPart][$rId]['targetMode'];
  677. }
  678. return $mode;
  679. }
  680. }