source of /sprout/Helpers/DocImport/DocImportDOCX.php<?php /* * Copyright (C) 2017 Karmabunny Pty Ltd. * * This file is a part of SproutCMS. * * SproutCMS is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Foundation, either * version 2 of the License, or (at your option) any later version. * * For more information, visit <http://getsproutcms.com>. */ namespace Sprout\Helpers\DocImport; use DOMDocument; use DOMElement; use ZipArchive; use Sprout\Helpers\Enc; class DocImportDOCX extends DocImport { private $zip; private $number_formats; private $styles; private $relationships; private $res; /** * The main load function for a document. * Throw an exception on error. * * @param string $filename The file. The file will exist, but may not be valid * @return string|DOMDocument $data Resultant XML data as a string or DOMDocument element */ public function load($filename) { $this->number_formats = []; $this->styles = []; $this->relationships = []; $this->res = []; $out = ''; $this->zip = new ZipArchive(); $this->zip->open($filename); $this->number_formats = $this->loadFormats(); $this->styles = $this->loadStyles(); $this->numbersFromStyles(); $this->relationships = $this->loadRelationships(); $doc = new DOMDocument(); $doc->loadXML($this->zip->getFromName('word/document.xml')); $body = $doc->firstChild->getElementsByTagName('body'); if ($body->length == 0) return null; $body = $body->item(0); if (!$body instanceof DOMElement) return null; if ($body->tagName != 'w:body') return null; if ($body->childNodes->length == 0) return null; $out .= '<?xml version="1.0" encoding="UTF-8" ?>' . PHP_EOL; $out .= '<doc>' . PHP_EOL; $out .= '<body>' . PHP_EOL; $out .= $this->block($body); $out .= '</body>' . PHP_EOL; foreach ($this->res as $name => $data) { } $out .= '</doc>'; $this->zip->close(); return $out; } /** * Validates element as block display * * @param DOMElement $elem * @return bool True Valid block element * @return bool False Invalid block element */ private function isValidBlockElem($elem) { if (!in_array($elem->tagName, ['w:p', 'w:tbl'])) return false; $runs = $this->renderBlockRuns($elem); if (strip_tags($runs, '<img>') == '') return false; return true; } /** * Draw block element * * @param DOMElement $elem * @return string HTML */ private function block($elem) { $list_stack = []; $list_fmt = null; $list_lvl = 0; $out = []; foreach ($elem->childNodes as $para) { if (!$para instanceof DOMElement) continue; // Tables if ($para->tagName == 'w:tbl') { $out[] = "</{$tag}>"; } $out[] = $this->drawTable($para); continue; } // Handle tags like w:bookmarkStart, as well as esoteric ones like w:moveToRangeEnd if ($para->tagName != 'w:p') { continue; } // Render the inner tags, drop if empty $runs = $this->renderBlockRuns($para); // Determine the style $style = $this->determineStyle($para); // Look for style changes (para <-> list) if ($style['number_format'] != $list_fmt or $style['number_level'] != $list_lvl) { $listtag = $this->determineListTag($style); $out[] = "<{$listtag}>"; $list_fmt = $style['number_format']; $list_lvl = $style['number_level']; } // Find the next sibling which is a tag we support (paragraphs and tables) $nextSibling = $para->nextSibling; while ($nextSibling and !$this->isValidBlockElem($nextSibling)) { $nextSibling = $nextSibling->nextSibling; } // Take a look at the next el to see if we will be raising or dropping soon. $lvlraise = false; $lvldrop = false; $typechange = false; if ($style['number_format'] and $nextSibling) { $nextstyle = $this->determineStyle($nextSibling); if ($nextstyle['number_format'] != $style['number_format'] and $nextstyle['number_level'] == $style['number_level']) { $typechange = true; if ($nextstyle['number_format'] == '') $lvldrop = true; } else if ($nextstyle['number_level'] < $style['number_level']) { $lvldrop = true; } else if ($nextstyle['number_level'] > $style['number_level']) { $lvlraise = true; } } // Render the list item or the paragraph if ($lvlraise) { $out[] = "<li>{$runs}"; } else if ($list_fmt) { $out[] = "<li>{$runs}</li>"; } else { $tag = $this->determineParaTag($style); if ($tag[0] == 'h') { $has_images = preg_match('!<img .+? />!', $runs, $image_tags); // Remove tags from the heading // Headings in ALL CAPS get converted to Title Case. } // If we actually got any content, output it if ($heading) { $out[] = "<{$tag}>{$heading}</{$tag}>"; } // If we found images, inject them in a P tag afterwards if ($has_images) { $out[] = '<p>' . implode('', $image_tags) . '</p>'; } } else { $out[] = "<{$tag}>{$runs}</{$tag}>"; } } // If there was a type change or level drop, pop the UL/OL element if ($typechange or $lvldrop) { $out[] = "</{$listtag}>"; } if ($lvldrop) { $list_fmt = $nextstyle['number_format']; $list_lvl = $nextstyle['number_level']; if (count($list_stack)) $out[] = "</li>"; } } // Pop any remaining UL or OL elements $out[] = "</{$tag}>"; } return implode(PHP_EOL , $out) . PHP_EOL ; } /** * Draw a w:tbl element * * @param DOMElement $elem * @return string HTML table */ private function drawTable($elem) { $out = '<table class="table--content-standard">' . PHP_EOL; $rows = $elem->getElementsByTagName('tr'); foreach ($rows as $row) { $out .= '<tr>' . PHP_EOL; $cells = $row->getElementsByTagName('tc'); foreach ($cells as $cell) { $paras = $cell->getElementsByTagName('p'); $rendered = []; foreach($paras as $p) { $rendered[] = $this->renderBlockRuns($p); } $out .= '<td>'; $out .= implode('<br/>', $rendered); $out .= '</td>' . PHP_EOL; } $out .= '</tr>' . PHP_EOL; } $out .= '</table>' . PHP_EOL; return $out; } /** * Render all the runs (i.e. w:r elements) for a given block element * * You would think this would be a simple draw_runs call on the getElementsByTagName, * but we would never actually have it _that_ easy... * * @param DOMElement $block * @return string XML tags representing the run content */ private function renderBlockRuns($block) { $runs = []; foreach ($block->childNodes as $child) { if (! $child instanceof DOMElement) continue; if ($child->tagName == 'w:r') { $runs[] = new DocImportDOCXRun($child); } else if ($child->tagName == 'w:hyperlink') { $href = $this->relationships[$child->getAttribute('r:id')]; $run = new DocImportDOCXRun($child); if ($href) { $run->rendered = '<a href="' . Enc::xml($href) . '">' . $this->renderBlockRuns($child) . '</a>'; } else { $run->rendered = $this->renderBlockRuns($child); } $runs[] = $run; } else if ($child->tagName == 'w:smartTag' or $child->tagName == 'w:ins') { $childRuns = $child->getElementsByTagName('r'); foreach ($childRuns as $run) { $runs[] = new DocImportDOCXRun($run); } } } return trim($this->drawRuns($runs)); } /** * Output one or more `w:r` elements * * @param array $runs * @return string */ private function drawRuns($runs) { $out = ''; $currBold = false; $currItalic = false; $currHyperlink = false; $currSubscript = false; $currSuperscript = false; $tagStack = []; foreach ($runs as $run) { if (!empty($run->rendered)) { $out .= $run->rendered; continue; } $runElem = $run->elem; $newBold = false; $newItalic = false; $newSubscript = false; $newSuperscript = false; $symbolDecode = false; $rpr = $runElem->getElementsByTagName('rPr'); if ($rpr->length) { foreach ($rpr->item(0)->childNodes as $node) { if (! $node instanceof DOMElement) continue; switch ($node->tagName) { case 'w:rStyle': $style = $this->styles[$node->getAttribute('w:val')]; if ($style) { if (!empty($style['bold'])) $newBold = true; if (!empty($style['italic'])) $newItalic = true; } break; case 'w:b': $newBold = ($node->getAttribute('w:val') !== 'false' and $node->getAttribute('w:val') !== '0'); break; case 'w:i': $newItalic = ($node->getAttribute('w:val') !== 'false' and $node->getAttribute('w:val') !== '0'); break; case 'w:vertAlign': if ($node->getAttribute('w:val') == 'subscript') { $newSubscript = true; } else if ($node->getAttribute('w:val') == 'superscript') { $newSuperscript = true; } break; case 'w:rFonts': if ($node->getAttribute('w:ascii') == 'Symbol') { $symbolDecode = true; } break; } } } // Determine tags to close $needToClose = []; if ($currItalic and !$newItalic) $needToClose[] = 'i'; if ($currBold and !$newBold) $needToClose[] = 'b'; if ($currSubscript and !$newSubscript) $needToClose[] = 'sub'; if ($currSuperscript and !$newSuperscript) $needToClose[] = 'sup'; // Close the whole tag stack, then reopen any which are meant to be open if (count($needToClose)) { $reopen = []; if (!in_array($tag, $needToClose)) $reopen[] = $tag; $out .= '</' . $tag . '>'; } foreach ($reopen as $tag) { $out .= '<' . $tag . '>'; } $tagStack = $reopen; } // Open new tags if (!$currBold and $newBold) { $out .= '<b>'; $tagStack[] = 'b'; } if (!$currItalic and $newItalic) { $out .= '<i>'; $tagStack[] = 'i'; } if (!$currSubscript and $newSubscript) { $out .= '<sub>'; $tagStack[] = 'sub'; } if (!$currSuperscript and $newSuperscript) { $out .= '<sup>'; $tagStack[] = 'sup'; } // Update state variables $currBold = $newBold; $currItalic = $newItalic; $currSubscript = $newSubscript; $currSuperscript = $newSuperscript; // Output the text, br and graphic elements $texts = $runElem->childNodes; foreach ($texts as $node) { if ($node->tagName == 'w:t') { if ($symbolDecode) { $out .= Enc::xml($this->symbolSanitizeString($node->firstChild->data)); } else { $out .= Enc::xml($node->firstChild->data); } } else if ($node->tagName == 'w:drawing') { $out .= $this->drawing($node); } else if ($node->tagName == 'w:pict') { $out .= $this->pict($node); } else if ($node->tagName == 'w:br') { $out .= '<br/>'; } else if ($node->tagName == 'w:tab') { $out .= "\t"; } } } // Close any remaining tags $out .= '</' . $tag . '>'; } // Clean up styled words with unstyled spaces // Clean up unstyled words with styled spaces // Remove multiple BRs in a row // Move BRs outside B and I tags $out = preg_replace('!<br/></([bi])>!', '</$1><br/>', $out); // Remove trailing and leading BRs return $out; } /** * Load the number formats from numbering.xml * * @return array */ private function loadFormats() { $out = []; if (! $this->zip->statName('word/numbering.xml')) return []; $doc = new DOMDocument(); $doc->loadXML($this->zip->getFromName('word/numbering.xml')); $tmp = []; $abstractnums = $doc->getElementsByTagName('abstractNum'); foreach ($abstractnums as $elem) { $id = $elem->getAttribute('w:abstractNumId'); $abstractnum = []; $e = $elem->getElementsByTagName('numFmt'); if ($e->length) { $abstractnum['numFmt'] = $e->item(0)->getAttribute('w:val'); } $e = $elem->getElementsByTagName('numStyleLink'); if ($e->length) { $abstractnum['styleName'] = $e->item(0)->getAttribute('w:val'); } $tmp[$id] = $abstractnum; } $nums = $doc->getElementsByTagName('num'); foreach ($nums as $elem) { $id = $elem->getAttribute('w:numId'); $e = $elem->getElementsByTagName('abstractNumId'); $e = $e->item(0)->getAttribute('w:val'); if (! isset($tmp[$e])) continue; $out[$id] = $tmp[$e]; } return $out; } /** * Load styles * * @return array */ private function loadStyles() { $out = []; if (! $this->zip->statName('word/styles.xml')) return []; $doc = new DOMDocument(); $doc->loadXML($this->zip->getFromName('word/styles.xml')); $elems = $doc->getElementsByTagName('style'); foreach ($elems as $elem) { $id = $elem->getAttribute('w:styleId'); $out[$id] = [ 'name' => $elem->getElementsByTagName('name')->item(0)->getAttribute('w:val'), ]; // Numbering style $numid = $elem->getElementsByTagName('numId'); if ($numid->length) { $out[$id]['numid'] = $numid->item(0)->getAttribute('w:val'); } // Bold tag $bold = $elem->getElementsByTagName('b'); if ($bold->length != 0 and $bold->item(0)->getAttribute('w:val') !== 'false' and $bold->item(0)->getAttribute('w:val') !== '0') { $out[$id]['bold'] = true; } // Italic tag $italic = $elem->getElementsByTagName('i'); if ($italic->length != 0 and $italic->item(0)->getAttribute('w:val') !== 'false' and $italic->item(0)->getAttribute('w:val') !== '0') { $out[$id]['italic'] = true; } // Base style $base = $elem->getElementsByTagName('basedOn'); if ($base->length) { $out[$id]['based_on_id'] = $base->item(0)->getAttribute('w:val'); } } foreach ($out as $index => $row) { if (isset($row['based_on_id'])) { $out[$index]['based_on_names'] = $this->flattenBasedOnTree($out, $row); } } return $out; } /** * Walk the chain of styles via the "based on" field to generate a list of names * * @param array $styles * @param array $heading * @return array List of names */ private function flattenBasedOnTree(&$styles, $heading) { if (isset($heading['based_on_id'])) { if (isset($styles[$heading['based_on_id']])) { $parent = $styles[$heading['based_on_id']]; $chain = $this->flattenBasedOnTree($styles, $parent); $chain[] = $parent['name']; return $chain; } } return null; } /** * Sometimes a numbering format refers to a style, the style itself contains the actual number format * This function dereferences the number formats back again * * @return void */ private function numbersFromStyles() { foreach ($this->number_formats as $idx => &$num) { if (isset($num['styleName'])) { $style = $this->styles[$num['styleName']]; if (! $style) continue; $numId = $style['numid']; if (! $numId) continue; $upstreamFormat = $this->number_formats[$numId]; if (! $upstreamFormat) continue; $num['numFmt'] = $upstreamFormat['numFmt']; } } } /** * Relationships is how the main document.xml links together with various media files etc * * @return array */ private function loadRelationships() { $out = []; $doc = new DOMDocument(); $doc->loadXML($this->zip->getFromName('word/_rels/document.xml.rels')); $elems = $doc->getElementsByTagName('Relationship'); foreach ($elems as $elem) { $id = $elem->getAttribute('Id'); $target = $elem->getAttribute('Target'); $out[$id] = $target; } return $out; } /** * For a given paragraph element, determine the finalised style in use * * @param DOMElement $elem * @return array */ private function determineStyle($elem) { $out = []; $out['style'] = null; $out['style_name'] = null; $out['based_on'] = null; $out['number_format'] = null; $out['number_level'] = 0; // Get style id and name $style = $elem->getElementsByTagName('pStyle'); if ($style->length) { $out['style'] = $style->item(0)->getAttribute('w:val'); $out['style_name'] = $this->styles[$out['style']]['name']; $out['based_on'] = @$this->styles[$out['style']]['based_on_names']; } // Apply details from the style if (isset($this->styles[$out['style']])) { $style = $this->styles[$out['style']]; if (isset($style['numid'])) { $out['number_format'] = $this->number_formats[$style['numid']]['numFmt']; } } // Apply local numbering $num = $elem->getElementsByTagName('numPr'); if ($num->length) { $id = $num->item(0)->getElementsByTagName('numId'); if ($id->length) { $numberId = $id->item(0)->getAttribute('w:val'); if (isset($this->number_formats[$numberId]['numFmt'])) { $out['number_format'] = $this->number_formats[$numberId]['numFmt']; } } $id = $num->item(0)->getElementsByTagName('ilvl'); if ($id->length) { $out['number_level'] = $id->item(0)->getAttribute('w:val'); } } // If this is a heading style with numbering, kill the numbering $expected_tag = $this->determineParaTag($out); if ($expected_tag[0] == 'h') { $out['number_format'] = null; $out['number_level'] = 0; } // If this uses the numberfing format "none", kill the numbering if ($out['number_format'] == 'none') { $out['number_format'] = null; $out['number_level'] = 0; } return $out; } /** * For a given style tag, return a paragraph tag (either 'p' or 'h1', 'h2', etc) * * @param array $style * @return string Tag name */ private function determineParaTag($style) { // If the style itself is a heading if (strpos($name, 'heading 1') === 0) return 'h1'; if (strpos($name, 'heading 2') === 0) return 'h2'; if (strpos($name, 'heading 3') === 0) return 'h3'; if (strpos($name, 'heading 4') === 0) return 'h4'; if (strpos($name, 'heading 5') === 0) return 'h5'; if (strpos($name, 'heading 6') === 0) return 'h6'; // If one of the styles it's based on is a heading foreach ($style['based_on'] as $name) { if (strpos($name, 'heading 1') === 0) return 'h1'; if (strpos($name, 'heading 2') === 0) return 'h2'; if (strpos($name, 'heading 3') === 0) return 'h3'; if (strpos($name, 'heading 4') === 0) return 'h4'; if (strpos($name, 'heading 5') === 0) return 'h5'; if (strpos($name, 'heading 6') === 0) return 'h6'; } } return 'p'; } /** * For a given style tag, return a list tag (either 'ul' or 'ol') * * @param array $style * @return string Tag name */ private function determineListTag($style) { if ($style['number_format'] == 'bullet') return 'ul'; return 'ol'; } /** * Render a w:drawing object, i.e. an image * * @param DOMElement $elem * @return string HTML img tag */ private function drawing($elem) { $graphic = $elem->getElementsByTagName('graphic'); if (! $graphic->length) return; $graphic = $graphic->item(0); $blip = $graphic->getElementsByTagName('blip'); if (! $blip->length) return; $id = $blip->item(0)->getAttribute('r:embed'); // Check resource exists $stat = $this->zip->statName('word/' . $this->relationships[$id]); if (! $stat) return; // Get image size props $ext = $graphic->getElementsByTagName('ext')->item(0); $sizeX = $this->EMUtoPX($ext->getAttribute('cx')); $sizeY = $this->EMUtoPX($ext->getAttribute('cy')); // Check ext $resname = basename($this->relationships[$id]); if (!in_array($fileext, ['jpg', 'jpeg', 'gif', 'png'])) { return '<img error="unsupported-type" res="' . $resname . '" width="' . round($sizeX) . '" height="' . round($sizeY) . '" />'; } // Load resource if (empty($this->res[$resname])) { $this->res[$resname] = $this->zip->getFromName('word/' . $this->relationships[$id]); } return '<img rel="' . $resname . '" width="' . round($sizeX, 1) . '" height="' . round($sizeY, 1) . '" />'; } /** * Render a w:pict object, i.e. an image * * @param DOMElement $elem * @return string HTML img tag */ private function pict($elem) { $shape = $elem->getElementsByTagName('shape'); if (! $shape->length) return; $shape = $shape->item(0); $imagedata = $shape->getElementsByTagName('imagedata'); if (! $imagedata->length) return; $id = $imagedata->item(0)->getAttribute('r:id'); // Check resource exists $stat = $this->zip->statName('word/' . $this->relationships[$id]); if (! $stat) return; // Get image size props $css = $shape->getAttribute('style'); $css = $this->parseCss($css); if (preg_match('/[0-9]+/', $css['width'], $matches)) $sizeX = $matches[0]; if (preg_match('/[0-9]+/', $css['height'], $matches)) $sizeY = $matches[0]; // Check ext $resname = basename($this->relationships[$id]); if (!in_array($fileext, ['jpg', 'jpeg', 'gif', 'png'])) { return '<img error="unsupported-type" res="' . $resname . '" width="' . round($sizeX) . '" height="' . round($sizeY) . '" />'; } // Load resource if (empty($this->res[$resname])) { $this->res[$resname] = $this->zip->getFromName('word/' . $this->relationships[$id]); } return '<img rel="' . $resname . '" width="' . round($sizeX, 1) . '" height="' . round($sizeY, 1) . '" />'; } /** * Convert 'Symbol' font Private-Use-Area characters into real characters * * @param string $string * @return string */ public function symbolSanitizeString($string) { '/([\x{f020}-\x{f0fe}]{1})/u', [$this, 'symbolUnicodeToUtf8Entity'], $string ); } /** * Regular expression callback for Symbol font conversion * * @param string $wchar * @return string */ public function symbolUnicodeToUtf8Entity($wchar) { $charcode = self::$symbol_font_map[$conv]; } /** * Parse given css * * @param string $css * @return array */ private function parseCss($css) { $out = []; foreach ($rules as $r) { if ($key and $val) { } } return $out; } /** * EM units to pixels * * @param int|float $emu * @param int $dpi * @return float Pixel value */ private function EMUtoPX($emu, $dpi = 72) { return $emu / 914400 * $dpi; } /** * Mapping between PUA for Symbol font to regular characters * * Key - UTF-8 encoded bytes * Value - Widechar bytes **/ static $symbol_font_map = [ 15696032 => 32, 15696033 => 33, 15696034 => 8704, 15696035 => 35, 15696036 => 8707, 15696037 => 37, 15696038 => 38, 15696039 => 8715, 15696040 => 40, 15696041 => 41, 15696042 => 8727, 15696043 => 43, 15696044 => 44, 15696045 => 8722, 15696046 => 46, 15696047 => 47, 15696048 => 48, 15696049 => 49, 15696050 => 50, 15696051 => 51, 15696052 => 52, 15696053 => 53, 15696054 => 54, 15696055 => 55, 15696056 => 56, 15696057 => 57, 15696058 => 58, 15696059 => 59, 15696060 => 60, 15696061 => 61, 15696062 => 62, 15696063 => 63, 15696256 => 8773, 15696257 => 913, 15696258 => 914, 15696259 => 935, 15696260 => 916, 15696261 => 917, 15696262 => 934, 15696263 => 915, 15696264 => 919, 15696265 => 921, 15696266 => 977, 15696267 => 922, 15696268 => 923, 15696269 => 924, 15696270 => 925, 15696271 => 927, 15696272 => 928, 15696273 => 920, 15696274 => 929, 15696275 => 931, 15696276 => 932, 15696277 => 933, 15696278 => 962, 15696279 => 937, 15696280 => 926, 15696281 => 936, 15696282 => 918, 15696283 => 91, 15696284 => 8756, 15696285 => 93, 15696286 => 8869, 15696287 => 95, 15696288 => 63717, 15696289 => 945, 15696290 => 946, 15696291 => 967, 15696292 => 948, 15696293 => 949, 15696294 => 966, 15696295 => 947, 15696296 => 951, 15696297 => 953, 15696298 => 981, 15696299 => 954, 15696300 => 955, 15696301 => 956, 15696302 => 957, 15696303 => 959, 15696304 => 960, 15696305 => 952, 15696306 => 961, 15696307 => 963, 15696308 => 964, 15696309 => 965, 15696310 => 982, 15696311 => 969, 15696312 => 958, 15696313 => 968, 15696314 => 950, 15696315 => 123, 15696316 => 124, 15696317 => 125, 15696318 => 8764, 15696544 => 8364, 15696545 => 978, 15696546 => 8242, 15696547 => 8804, 15696548 => 8260, 15696549 => 8734, 15696550 => 402, 15696551 => 9827, 15696552 => 9830, 15696553 => 9829, 15696554 => 9824, 15696555 => 8596, 15696556 => 8592, 15696557 => 8593, 15696558 => 8594, 15696559 => 8595, 15696560 => 176, 15696561 => 177, 15696562 => 8243, 15696563 => 8805, 15696564 => 215, 15696565 => 8733, 15696566 => 8706, 15696567 => 8226, 15696568 => 247, 15696569 => 8800, 15696570 => 8801, 15696571 => 8776, 15696572 => 8230, 15696573 => 63718, 15696574 => 63719, 15696575 => 8629, 15696768 => 8501, 15696769 => 8465, 15696770 => 8476, 15696771 => 8472, 15696772 => 8855, 15696773 => 8853, 15696774 => 8709, 15696775 => 8745, 15696776 => 8746, 15696777 => 8835, 15696778 => 8839, 15696779 => 8836, 15696780 => 8834, 15696781 => 8838, 15696782 => 8712, 15696783 => 8713, 15696784 => 8736, 15696785 => 8711, 15696786 => 63194, 15696787 => 63193, 15696788 => 63195, 15696789 => 8719, 15696790 => 8730, 15696791 => 8901, 15696792 => 172, 15696793 => 8743, 15696794 => 8744, 15696795 => 8660, 15696796 => 8656, 15696797 => 8657, 15696798 => 8658, 15696799 => 8659, 15696800 => 9674, 15696801 => 9001, 15696802 => 63720, 15696803 => 63721, 15696804 => 63722, 15696805 => 8721, 15696806 => 63723, 15696807 => 63724, 15696808 => 63725, 15696809 => 63726, 15696810 => 63727, 15696811 => 63728, 15696812 => 63729, 15696813 => 63730, 15696814 => 63731, 15696815 => 63732, 15696817 => 9002, 15696818 => 8747, 15696819 => 8992, 15696820 => 63733, 15696821 => 8993, 15696822 => 63734, 15696823 => 63735, 15696824 => 63736, 15696825 => 63737, 15696826 => 63738, 15696827 => 63739, 15696828 => 63740, 15696829 => 63741, 15696830 => 63742, ]; } class DocImportDOCXRun { public $elem; public $hyperlink; public $rendered; /** * Constructor * * @param DOMElement $elem */ public function __construct($elem) { $this->elem = $elem; } }
|