PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 3.5.0
Visualizer – Tables & Charts Manager with Built-in AI Generator v3.5.0
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.10.1 3.10.10 3.10.11 3.10.12 3.10.13 3.10.14 3.10.15 3.10.2 3.10.3 All 149 releases
visualizer / vendor / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Reader / Xlsx.php

Xlsx.php in Visualizer – Tables & Charts Manager with Built-in AI Generator 3.5.0, at vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php

2,030 lines 113.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace PhpOffice\PhpSpreadsheet\Reader;
4
5 use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6 use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
7 use PhpOffice\PhpSpreadsheet\NamedRange;
8 use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
9 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\AutoFilter;
10 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Chart;
11 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ColumnAndRowAttributes;
12 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ConditionalStyles;
13 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\DataValidations;
14 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Hyperlinks;
15 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\PageSetup;
16 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader;
17 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions;
18 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews;
19 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles;
20 use PhpOffice\PhpSpreadsheet\ReferenceHelper;
21 use PhpOffice\PhpSpreadsheet\RichText\RichText;
22 use PhpOffice\PhpSpreadsheet\Settings;
23 use PhpOffice\PhpSpreadsheet\Shared\Date;
24 use PhpOffice\PhpSpreadsheet\Shared\Drawing;
25 use PhpOffice\PhpSpreadsheet\Shared\File;
26 use PhpOffice\PhpSpreadsheet\Shared\Font;
27 use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
28 use PhpOffice\PhpSpreadsheet\Spreadsheet;
29 use PhpOffice\PhpSpreadsheet\Style\Border;
30 use PhpOffice\PhpSpreadsheet\Style\Borders;
31 use PhpOffice\PhpSpreadsheet\Style\Color;
32 use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
33 use PhpOffice\PhpSpreadsheet\Style\Protection;
34 use PhpOffice\PhpSpreadsheet\Style\Style;
35 use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing;
36 use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
37 use SimpleXMLElement;
38 use XMLReader;
39 use ZipArchive;
40
41 class Xlsx extends BaseReader
42 {
43 /**
44 * ReferenceHelper instance.
45 *
46 * @var ReferenceHelper
47 */
48 private $referenceHelper;
49
50 /**
51 * Xlsx\Theme instance.
52 *
53 * @var Xlsx\Theme
54 */
55 private static $theme = null;
56
57 /**
58 * Create a new Xlsx Reader instance.
59 */
60 public function __construct()
61 {
62 parent::__construct();
63 $this->referenceHelper = ReferenceHelper::getInstance();
64 $this->securityScanner = XmlScanner::getInstance($this);
65 }
66
67 /**
68 * Can the current IReader read the file?
69 *
70 * @param string $pFilename
71 *
72 * @throws Exception
73 *
74 * @return bool
75 */
76 public function canRead($pFilename)
77 {
78 File::assertFile($pFilename);
79
80 $xl = false;
81 // Load file
82 $zip = new ZipArchive();
83 if ($zip->open($pFilename) === true) {
84 // check if it is an OOXML archive
85 $rels = simplexml_load_string(
86 $this->securityScanner->scan(
87 $this->getFromZipArchive($zip, '_rels/.rels')
88 ),
89 'SimpleXMLElement',
90 Settings::getLibXmlLoaderOptions()
91 );
92 if ($rels !== false) {
93 foreach ($rels->Relationship as $rel) {
94 switch ($rel['Type']) {
95 case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument':
96 if (basename($rel['Target']) == 'workbook.xml') {
97 $xl = true;
98 }
99
100 break;
101 }
102 }
103 }
104 $zip->close();
105 }
106
107 return $xl;
108 }
109
110 /**
111 * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
112 *
113 * @param string $pFilename
114 *
115 * @throws Exception
116 *
117 * @return array
118 */
119 public function listWorksheetNames($pFilename)
120 {
121 File::assertFile($pFilename);
122
123 $worksheetNames = [];
124
125 $zip = new ZipArchive();
126 $zip->open($pFilename);
127
128 // The files we're looking at here are small enough that simpleXML is more efficient than XMLReader
129 //~ http://schemas.openxmlformats.org/package/2006/relationships");
130 $rels = simplexml_load_string(
131 $this->securityScanner->scan($this->getFromZipArchive($zip, '_rels/.rels'))
132 );
133 foreach ($rels->Relationship as $rel) {
134 switch ($rel['Type']) {
135 case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument':
136 //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main"
137 $xmlWorkbook = simplexml_load_string(
138 $this->securityScanner->scan($this->getFromZipArchive($zip, "{$rel['Target']}"))
139 );
140
141 if ($xmlWorkbook->sheets) {
142 foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
143 // Check if sheet should be skipped
144 $worksheetNames[] = (string) $eleSheet['name'];
145 }
146 }
147 }
148 }
149
150 $zip->close();
151
152 return $worksheetNames;
153 }
154
155 /**
156 * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
157 *
158 * @param string $pFilename
159 *
160 * @throws Exception
161 *
162 * @return array
163 */
164 public function listWorksheetInfo($pFilename)
165 {
166 File::assertFile($pFilename);
167
168 $worksheetInfo = [];
169
170 $zip = new ZipArchive();
171 $zip->open($pFilename);
172
173 //~ http://schemas.openxmlformats.org/package/2006/relationships"
174 $rels = simplexml_load_string(
175 $this->securityScanner->scan($this->getFromZipArchive($zip, '_rels/.rels')),
176 'SimpleXMLElement',
177 Settings::getLibXmlLoaderOptions()
178 );
179 foreach ($rels->Relationship as $rel) {
180 if ($rel['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument') {
181 $dir = dirname($rel['Target']);
182
183 //~ http://schemas.openxmlformats.org/package/2006/relationships"
184 $relsWorkbook = simplexml_load_string(
185 $this->securityScanner->scan(
186 $this->getFromZipArchive($zip, "$dir/_rels/" . basename($rel['Target']) . '.rels')
187 ),
188 'SimpleXMLElement',
189 Settings::getLibXmlLoaderOptions()
190 );
191 $relsWorkbook->registerXPathNamespace('rel', 'http://schemas.openxmlformats.org/package/2006/relationships');
192
193 $worksheets = [];
194 foreach ($relsWorkbook->Relationship as $ele) {
195 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet') {
196 $worksheets[(string) $ele['Id']] = $ele['Target'];
197 }
198 }
199
200 //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main"
201 $xmlWorkbook = simplexml_load_string(
202 $this->securityScanner->scan(
203 $this->getFromZipArchive($zip, "{$rel['Target']}")
204 ),
205 'SimpleXMLElement',
206 Settings::getLibXmlLoaderOptions()
207 );
208 if ($xmlWorkbook->sheets) {
209 $dir = dirname($rel['Target']);
210 /** @var SimpleXMLElement $eleSheet */
211 foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
212 $tmpInfo = [
213 'worksheetName' => (string) $eleSheet['name'],
214 'lastColumnLetter' => 'A',
215 'lastColumnIndex' => 0,
216 'totalRows' => 0,
217 'totalColumns' => 0,
218 ];
219
220 $fileWorksheet = $worksheets[(string) self::getArrayItem($eleSheet->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'id')];
221
222 $xml = new XMLReader();
223 $xml->xml(
224 $this->securityScanner->scanFile(
225 'zip://' . File::realpath($pFilename) . '#' . "$dir/$fileWorksheet"
226 ),
227 null,
228 Settings::getLibXmlLoaderOptions()
229 );
230 $xml->setParserProperty(2, true);
231
232 $currCells = 0;
233 while ($xml->read()) {
234 if ($xml->name == 'row' && $xml->nodeType == XMLReader::ELEMENT) {
235 $row = $xml->getAttribute('r');
236 $tmpInfo['totalRows'] = $row;
237 $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
238 $currCells = 0;
239 } elseif ($xml->name == 'c' && $xml->nodeType == XMLReader::ELEMENT) {
240 ++$currCells;
241 }
242 }
243 $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
244 $xml->close();
245
246 $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
247 $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
248
249 $worksheetInfo[] = $tmpInfo;
250 }
251 }
252 }
253 }
254
255 $zip->close();
256
257 return $worksheetInfo;
258 }
259
260 private static function castToBoolean($c)
261 {
262 $value = isset($c->v) ? (string) $c->v : null;
263 if ($value == '0') {
264 return false;
265 } elseif ($value == '1') {
266 return true;
267 }
268
269 return (bool) $c->v;
270 }
271
272 private static function castToError($c)
273 {
274 return isset($c->v) ? (string) $c->v : null;
275 }
276
277 private static function castToString($c)
278 {
279 return isset($c->v) ? (string) $c->v : null;
280 }
281
282 private function castToFormula($c, $r, &$cellDataType, &$value, &$calculatedValue, &$sharedFormulas, $castBaseType)
283 {
284 $cellDataType = 'f';
285 $value = "={$c->f}";
286 $calculatedValue = self::$castBaseType($c);
287
288 // Shared formula?
289 if (isset($c->f['t']) && strtolower((string) $c->f['t']) == 'shared') {
290 $instance = (string) $c->f['si'];
291
292 if (!isset($sharedFormulas[(string) $c->f['si']])) {
293 $sharedFormulas[$instance] = ['master' => $r, 'formula' => $value];
294 } else {
295 $master = Coordinate::coordinateFromString($sharedFormulas[$instance]['master']);
296 $current = Coordinate::coordinateFromString($r);
297
298 $difference = [0, 0];
299 $difference[0] = Coordinate::columnIndexFromString($current[0]) - Coordinate::columnIndexFromString($master[0]);
300 $difference[1] = $current[1] - $master[1];
301
302 $value = $this->referenceHelper->updateFormulaReferences($sharedFormulas[$instance]['formula'], 'A1', $difference[0], $difference[1]);
303 }
304 }
305 }
306
307 /**
308 * @param ZipArchive $archive
309 * @param string $fileName
310 *
311 * @return string
312 */
313 private function getFromZipArchive(ZipArchive $archive, $fileName = '')
314 {
315 // Root-relative paths
316 if (strpos($fileName, '//') !== false) {
317 $fileName = substr($fileName, strpos($fileName, '//') + 1);
318 }
319 $fileName = File::realpath($fileName);
320
321 // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
322 // so we need to load case-insensitively from the zip file
323
324 // Apache POI fixes
325 $contents = $archive->getFromName($fileName, 0, ZipArchive::FL_NOCASE);
326 if ($contents === false) {
327 $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE);
328 }
329
330 return $contents;
331 }
332
333 /**
334 * Loads Spreadsheet from file.
335 *
336 * @param string $pFilename
337 *
338 * @throws Exception
339 *
340 * @return Spreadsheet
341 */
342 public function load($pFilename)
343 {
344 File::assertFile($pFilename);
345
346 // Initialisations
347 $excel = new Spreadsheet();
348 $excel->removeSheetByIndex(0);
349 if (!$this->readDataOnly) {
350 $excel->removeCellStyleXfByIndex(0); // remove the default style
351 $excel->removeCellXfByIndex(0); // remove the default style
352 }
353 $unparsedLoadedData = [];
354
355 $zip = new ZipArchive();
356 $zip->open($pFilename);
357
358 // Read the theme first, because we need the colour scheme when reading the styles
359 //~ http://schemas.openxmlformats.org/package/2006/relationships"
360 $wbRels = simplexml_load_string(
361 $this->securityScanner->scan($this->getFromZipArchive($zip, 'xl/_rels/workbook.xml.rels')),
362 'SimpleXMLElement',
363 Settings::getLibXmlLoaderOptions()
364 );
365 foreach ($wbRels->Relationship as $rel) {
366 switch ($rel['Type']) {
367 case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme':
368 $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2'];
369 $themeOrderAdditional = count($themeOrderArray);
370
371 $xmlTheme = simplexml_load_string(
372 $this->securityScanner->scan($this->getFromZipArchive($zip, "xl/{$rel['Target']}")),
373 'SimpleXMLElement',
374 Settings::getLibXmlLoaderOptions()
375 );
376 if (is_object($xmlTheme)) {
377 $xmlThemeName = $xmlTheme->attributes();
378 $xmlTheme = $xmlTheme->children('http://schemas.openxmlformats.org/drawingml/2006/main');
379 $themeName = (string) $xmlThemeName['name'];
380
381 $colourScheme = $xmlTheme->themeElements->clrScheme->attributes();
382 $colourSchemeName = (string) $colourScheme['name'];
383 $colourScheme = $xmlTheme->themeElements->clrScheme->children('http://schemas.openxmlformats.org/drawingml/2006/main');
384
385 $themeColours = [];
386 foreach ($colourScheme as $k => $xmlColour) {
387 $themePos = array_search($k, $themeOrderArray);
388 if ($themePos === false) {
389 $themePos = $themeOrderAdditional++;
390 }
391 if (isset($xmlColour->sysClr)) {
392 $xmlColourData = $xmlColour->sysClr->attributes();
393 $themeColours[$themePos] = $xmlColourData['lastClr'];
394 } elseif (isset($xmlColour->srgbClr)) {
395 $xmlColourData = $xmlColour->srgbClr->attributes();
396 $themeColours[$themePos] = $xmlColourData['val'];
397 }
398 }
399 self::$theme = new Xlsx\Theme($themeName, $colourSchemeName, $themeColours);
400 }
401
402 break;
403 }
404 }
405
406 //~ http://schemas.openxmlformats.org/package/2006/relationships"
407 $rels = simplexml_load_string(
408 $this->securityScanner->scan($this->getFromZipArchive($zip, '_rels/.rels')),
409 'SimpleXMLElement',
410 Settings::getLibXmlLoaderOptions()
411 );
412
413 $propertyReader = new PropertyReader($this->securityScanner, $excel->getProperties());
414 foreach ($rels->Relationship as $rel) {
415 switch ($rel['Type']) {
416 case 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties':
417 $propertyReader->readCoreProperties($this->getFromZipArchive($zip, "{$rel['Target']}"));
418
419 break;
420 case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties':
421 $propertyReader->readExtendedProperties($this->getFromZipArchive($zip, "{$rel['Target']}"));
422
423 break;
424 case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties':
425 $propertyReader->readCustomProperties($this->getFromZipArchive($zip, "{$rel['Target']}"));
426
427 break;
428 //Ribbon
429 case 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility':
430 $customUI = $rel['Target'];
431 if ($customUI !== null) {
432 $this->readRibbon($excel, $customUI, $zip);
433 }
434
435 break;
436 case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument':
437 $dir = dirname($rel['Target']);
438 //~ http://schemas.openxmlformats.org/package/2006/relationships"
439 $relsWorkbook = simplexml_load_string(
440 $this->securityScanner->scan($this->getFromZipArchive($zip, "$dir/_rels/" . basename($rel['Target']) . '.rels')),
441 'SimpleXMLElement',
442 Settings::getLibXmlLoaderOptions()
443 );
444 $relsWorkbook->registerXPathNamespace('rel', 'http://schemas.openxmlformats.org/package/2006/relationships');
445
446 $sharedStrings = [];
447 $xpath = self::getArrayItem($relsWorkbook->xpath("rel:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings']"));
448 //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main"
449 $xmlStrings = simplexml_load_string(
450 $this->securityScanner->scan($this->getFromZipArchive($zip, "$dir/$xpath[Target]")),
451 'SimpleXMLElement',
452 Settings::getLibXmlLoaderOptions()
453 );
454 if (isset($xmlStrings, $xmlStrings->si)) {
455 foreach ($xmlStrings->si as $val) {
456 if (isset($val->t)) {
457 $sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t);
458 } elseif (isset($val->r)) {
459 $sharedStrings[] = $this->parseRichText($val);
460 }
461 }
462 }
463
464 $worksheets = [];
465 $macros = $customUI = null;
466 foreach ($relsWorkbook->Relationship as $ele) {
467 switch ($ele['Type']) {
468 case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet':
469 $worksheets[(string) $ele['Id']] = $ele['Target'];
470
471 break;
472 // a vbaProject ? (: some macros)
473 case 'http://schemas.microsoft.com/office/2006/relationships/vbaProject':
474 $macros = $ele['Target'];
475
476 break;
477 }
478 }
479
480 if ($macros !== null) {
481 $macrosCode = $this->getFromZipArchive($zip, 'xl/vbaProject.bin'); //vbaProject.bin always in 'xl' dir and always named vbaProject.bin
482 if ($macrosCode !== false) {
483 $excel->setMacrosCode($macrosCode);
484 $excel->setHasMacros(true);
485 //short-circuit : not reading vbaProject.bin.rel to get Signature =>allways vbaProjectSignature.bin in 'xl' dir
486 $Certificate = $this->getFromZipArchive($zip, 'xl/vbaProjectSignature.bin');
487 if ($Certificate !== false) {
488 $excel->setMacrosCertificate($Certificate);
489 }
490 }
491 }
492
493 $xpath = self::getArrayItem($relsWorkbook->xpath("rel:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles']"));
494 //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main"
495 $xmlStyles = simplexml_load_string(
496 $this->securityScanner->scan($this->getFromZipArchive($zip, "$dir/$xpath[Target]")),
497 'SimpleXMLElement',
498 Settings::getLibXmlLoaderOptions()
499 );
500
501 $styles = [];
502 $cellStyles = [];
503 $numFmts = null;
504 if ($xmlStyles && $xmlStyles->numFmts[0]) {
505 $numFmts = $xmlStyles->numFmts[0];
506 }
507 if (isset($numFmts) && ($numFmts !== null)) {
508 $numFmts->registerXPathNamespace('sml', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
509 }
510 if (!$this->readDataOnly && $xmlStyles) {
511 foreach ($xmlStyles->cellXfs->xf as $xf) {
512 $numFmt = NumberFormat::FORMAT_GENERAL;
513
514 if ($xf['numFmtId']) {
515 if (isset($numFmts)) {
516 $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
517
518 if (isset($tmpNumFmt['formatCode'])) {
519 $numFmt = (string) $tmpNumFmt['formatCode'];
520 }
521 }
522
523 // We shouldn't override any of the built-in MS Excel values (values below id 164)
524 // But there's a lot of naughty homebrew xlsx writers that do use "reserved" id values that aren't actually used
525 // So we make allowance for them rather than lose formatting masks
526 if ((int) $xf['numFmtId'] < 164 &&
527 NumberFormat::builtInFormatCode((int) $xf['numFmtId']) !== '') {
528 $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
529 }
530 }
531 $quotePrefix = false;
532 if (isset($xf['quotePrefix'])) {
533 $quotePrefix = (bool) $xf['quotePrefix'];
534 }
535
536 $style = (object) [
537 'numFmt' => $numFmt,
538 'font' => $xmlStyles->fonts->font[(int) ($xf['fontId'])],
539 'fill' => $xmlStyles->fills->fill[(int) ($xf['fillId'])],
540 'border' => $xmlStyles->borders->border[(int) ($xf['borderId'])],
541 'alignment' => $xf->alignment,
542 'protection' => $xf->protection,
543 'quotePrefix' => $quotePrefix,
544 ];
545 $styles[] = $style;
546
547 // add style to cellXf collection
548 $objStyle = new Style();
549 self::readStyle($objStyle, $style);
550 $excel->addCellXf($objStyle);
551 }
552
553 foreach (isset($xmlStyles->cellStyleXfs->xf) ? $xmlStyles->cellStyleXfs->xf : [] as $xf) {
554 $numFmt = NumberFormat::FORMAT_GENERAL;
555 if ($numFmts && $xf['numFmtId']) {
556 $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
557 if (isset($tmpNumFmt['formatCode'])) {
558 $numFmt = (string) $tmpNumFmt['formatCode'];
559 } elseif ((int) $xf['numFmtId'] < 165) {
560 $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
561 }
562 }
563
564 $cellStyle = (object) [
565 'numFmt' => $numFmt,
566 'font' => $xmlStyles->fonts->font[(int) ($xf['fontId'])],
567 'fill' => $xmlStyles->fills->fill[(int) ($xf['fillId'])],
568 'border' => $xmlStyles->borders->border[(int) ($xf['borderId'])],
569 'alignment' => $xf->alignment,
570 'protection' => $xf->protection,
571 'quotePrefix' => $quotePrefix,
572 ];
573 $cellStyles[] = $cellStyle;
574
575 // add style to cellStyleXf collection
576 $objStyle = new Style();
577 self::readStyle($objStyle, $cellStyle);
578 $excel->addCellStyleXf($objStyle);
579 }
580 }
581
582 $styleReader = new Styles($xmlStyles);
583 $styleReader->setStyleBaseData(self::$theme, $styles, $cellStyles);
584 $dxfs = $styleReader->dxfs($this->readDataOnly);
585 $styles = $styleReader->styles();
586
587 //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main"
588 $xmlWorkbook = simplexml_load_string(
589 $this->securityScanner->scan($this->getFromZipArchive($zip, "{$rel['Target']}")),
590 'SimpleXMLElement',
591 Settings::getLibXmlLoaderOptions()
592 );
593
594 // Set base date
595 if ($xmlWorkbook->workbookPr) {
596 Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
597 if (isset($xmlWorkbook->workbookPr['date1904'])) {
598 if (self::boolean((string) $xmlWorkbook->workbookPr['date1904'])) {
599 Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
600 }
601 }
602 }
603
604 // Set protection
605 $this->readProtection($excel, $xmlWorkbook);
606
607 $sheetId = 0; // keep track of new sheet id in final workbook
608 $oldSheetId = -1; // keep track of old sheet id in final workbook
609 $countSkippedSheets = 0; // keep track of number of skipped sheets
610 $mapSheetId = []; // mapping of sheet ids from old to new
611
612 $charts = $chartDetails = [];
613
614 if ($xmlWorkbook->sheets) {
615 /** @var SimpleXMLElement $eleSheet */
616 foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
617 ++$oldSheetId;
618
619 // Check if sheet should be skipped
620 if (isset($this->loadSheetsOnly) && !in_array((string) $eleSheet['name'], $this->loadSheetsOnly)) {
621 ++$countSkippedSheets;
622 $mapSheetId[$oldSheetId] = null;
623
624 continue;
625 }
626
627 // Map old sheet id in original workbook to new sheet id.
628 // They will differ if loadSheetsOnly() is being used
629 $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets;
630
631 // Load sheet
632 $docSheet = $excel->createSheet();
633 // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet
634 // references in formula cells... during the load, all formulae should be correct,
635 // and we're simply bringing the worksheet name in line with the formula, not the
636 // reverse
637 $docSheet->setTitle((string) $eleSheet['name'], false, false);
638 $fileWorksheet = $worksheets[(string) self::getArrayItem($eleSheet->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'id')];
639 //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main"
640 $xmlSheet = simplexml_load_string(
641 $this->securityScanner->scan($this->getFromZipArchive($zip, "$dir/$fileWorksheet")),
642 'SimpleXMLElement',
643 Settings::getLibXmlLoaderOptions()
644 );
645
646 $sharedFormulas = [];
647
648 if (isset($eleSheet['state']) && (string) $eleSheet['state'] != '') {
649 $docSheet->setSheetState((string) $eleSheet['state']);
650 }
651
652 if ($xmlSheet) {
653 if (isset($xmlSheet->sheetViews, $xmlSheet->sheetViews->sheetView)) {
654 $sheetViews = new SheetViews($xmlSheet->sheetViews->sheetView, $docSheet);
655 $sheetViews->load();
656 }
657
658 $sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheet);
659 $sheetViewOptions->load($this->getReadDataOnly());
660
661 (new ColumnAndRowAttributes($docSheet, $xmlSheet))
662 ->load($this->getReadFilter(), $this->getReadDataOnly());
663 }
664
665 if ($xmlSheet && $xmlSheet->sheetData && $xmlSheet->sheetData->row) {
666 $cIndex = 1; // Cell Start from 1
667 foreach ($xmlSheet->sheetData->row as $row) {
668 $rowIndex = 1;
669 foreach ($row->c as $c) {
670 $r = (string) $c['r'];
671 if ($r == '') {
672 $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex;
673 }
674 $cellDataType = (string) $c['t'];
675 $value = null;
676 $calculatedValue = null;
677
678 // Read cell?
679 if ($this->getReadFilter() !== null) {
680 $coordinates = Coordinate::coordinateFromString($r);
681
682 if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) {
683 $rowIndex += 1;
684
685 continue;
686 }
687 }
688
689 // Read cell!
690 switch ($cellDataType) {
691 case 's':
692 if ((string) $c->v != '') {
693 $value = $sharedStrings[(int) ($c->v)];
694
695 if ($value instanceof RichText) {
696 $value = clone $value;
697 }
698 } else {
699 $value = '';
700 }
701
702 break;
703 case 'b':
704 if (!isset($c->f)) {
705 $value = self::castToBoolean($c);
706 } else {
707 // Formula
708 $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToBoolean');
709 if (isset($c->f['t'])) {
710 $att = $c->f;
711 $docSheet->getCell($r)->setFormulaAttributes($att);
712 }
713 }
714
715 break;
716 case 'inlineStr':
717 if (isset($c->f)) {
718 $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError');
719 } else {
720 $value = $this->parseRichText($c->is);
721 }
722
723 break;
724 case 'e':
725 if (!isset($c->f)) {
726 $value = self::castToError($c);
727 } else {
728 // Formula
729 $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError');
730 }
731
732 break;
733 default:
734 if (!isset($c->f)) {
735 $value = self::castToString($c);
736 } else {
737 // Formula
738 $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToString');
739 }
740
741 break;
742 }
743
744 // read empty cells or the cells are not empty
745 if ($this->readEmptyCells || ($value !== null && $value !== '')) {
746 // Check for numeric values
747 if (is_numeric($value) && $cellDataType != 's') {
748 if ($value == (int) $value) {
749 $value = (int) $value;
750 } elseif ($value == (float) $value) {
751 $value = (float) $value;
752 }
753 }
754
755 // Rich text?
756 if ($value instanceof RichText && $this->readDataOnly) {
757 $value = $value->getPlainText();
758 }
759
760 $cell = $docSheet->getCell($r);
761 // Assign value
762 if ($cellDataType != '') {
763 $cell->setValueExplicit($value, $cellDataType);
764 } else {
765 $cell->setValue($value);
766 }
767 if ($calculatedValue !== null) {
768 $cell->setCalculatedValue($calculatedValue);
769 }
770
771 // Style information?
772 if ($c['s'] && !$this->readDataOnly) {
773 // no style index means 0, it seems
774 $cell->setXfIndex(isset($styles[(int) ($c['s'])]) ?
775 (int) ($c['s']) : 0);
776 }
777 }
778 $rowIndex += 1;
779 }
780 $cIndex += 1;
781 }
782 }
783
784 if (!$this->readDataOnly && $xmlSheet && $xmlSheet->conditionalFormatting) {
785 (new ConditionalStyles($docSheet, $xmlSheet, $dxfs))->load();
786 }
787
788 $aKeys = ['sheet', 'objects', 'scenarios', 'formatCells', 'formatColumns', 'formatRows', 'insertColumns', 'insertRows', 'insertHyperlinks', 'deleteColumns', 'deleteRows', 'selectLockedCells', 'sort', 'autoFilter', 'pivotTables', 'selectUnlockedCells'];
789 if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) {
790 foreach ($aKeys as $key) {
791 $method = 'set' . ucfirst($key);
792 $docSheet->getProtection()->$method(self::boolean((string) $xmlSheet->sheetProtection[$key]));
793 }
794 }
795
796 if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) {
797 $docSheet->getProtection()->setPassword((string) $xmlSheet->sheetProtection['password'], true);
798 if ($xmlSheet->protectedRanges->protectedRange) {
799 foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
800 $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true);
801 }
802 }
803 }
804
805 if ($xmlSheet && $xmlSheet->autoFilter && !$this->readDataOnly) {
806 (new AutoFilter($docSheet, $xmlSheet))->load();
807 }
808
809 if ($xmlSheet && $xmlSheet->mergeCells && $xmlSheet->mergeCells->mergeCell && !$this->readDataOnly) {
810 foreach ($xmlSheet->mergeCells->mergeCell as $mergeCell) {
811 $mergeRef = (string) $mergeCell['ref'];
812 if (strpos($mergeRef, ':') !== false) {
813 $docSheet->mergeCells((string) $mergeCell['ref']);
814 }
815 }
816 }
817
818 if ($xmlSheet && !$this->readDataOnly) {
819 $unparsedLoadedData = (new PageSetup($docSheet, $xmlSheet))->load($unparsedLoadedData);
820 }
821
822 if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) {
823 (new DataValidations($docSheet, $xmlSheet))->load();
824 }
825
826 // unparsed sheet AlternateContent
827 if ($xmlSheet && !$this->readDataOnly) {
828 $mc = $xmlSheet->children('http://schemas.openxmlformats.org/markup-compatibility/2006');
829 if ($mc->AlternateContent) {
830 foreach ($mc->AlternateContent as $alternateContent) {
831 $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['AlternateContents'][] = $alternateContent->asXML();
832 }
833 }
834 }
835
836 // Add hyperlinks
837 if (!$this->readDataOnly) {
838 $hyperlinkReader = new Hyperlinks($docSheet);
839 // Locate hyperlink relations
840 $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
841 if ($zip->locateName($relationsFileName)) {
842 //~ http://schemas.openxmlformats.org/package/2006/relationships"
843 $relsWorksheet = simplexml_load_string(
844 $this->securityScanner->scan(
845 $this->getFromZipArchive($zip, $relationsFileName)
846 ),
847 'SimpleXMLElement',
848 Settings::getLibXmlLoaderOptions()
849 );
850 $hyperlinkReader->readHyperlinks($relsWorksheet);
851 }
852
853 // Loop through hyperlinks
854 if ($xmlSheet && $xmlSheet->hyperlinks) {
855 $hyperlinkReader->setHyperlinks($xmlSheet->hyperlinks);
856 }
857 }
858
859 // Add comments
860 $comments = [];
861 $vmlComments = [];
862 if (!$this->readDataOnly) {
863 // Locate comment relations
864 if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) {
865 //~ http://schemas.openxmlformats.org/package/2006/relationships"
866 $relsWorksheet = simplexml_load_string(
867 $this->securityScanner->scan(
868 $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')
869 ),
870 'SimpleXMLElement',
871 Settings::getLibXmlLoaderOptions()
872 );
873 foreach ($relsWorksheet->Relationship as $ele) {
874 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments') {
875 $comments[(string) $ele['Id']] = (string) $ele['Target'];
876 }
877 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing') {
878 $vmlComments[(string) $ele['Id']] = (string) $ele['Target'];
879 }
880 }
881 }
882
883 // Loop through comments
884 foreach ($comments as $relName => $relPath) {
885 // Load comments file
886 $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
887 $commentsFile = simplexml_load_string(
888 $this->securityScanner->scan($this->getFromZipArchive($zip, $relPath)),
889 'SimpleXMLElement',
890 Settings::getLibXmlLoaderOptions()
891 );
892
893 // Utility variables
894 $authors = [];
895
896 // Loop through authors
897 foreach ($commentsFile->authors->author as $author) {
898 $authors[] = (string) $author;
899 }
900
901 // Loop through contents
902 foreach ($commentsFile->commentList->comment as $comment) {
903 if (!empty($comment['authorId'])) {
904 $docSheet->getComment((string) $comment['ref'])->setAuthor($authors[(string) $comment['authorId']]);
905 }
906 $docSheet->getComment((string) $comment['ref'])->setText($this->parseRichText($comment->text));
907 }
908 }
909
910 // later we will remove from it real vmlComments
911 $unparsedVmlDrawings = $vmlComments;
912
913 // Loop through VML comments
914 foreach ($vmlComments as $relName => $relPath) {
915 // Load VML comments file
916 $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
917
918 try {
919 $vmlCommentsFile = simplexml_load_string(
920 $this->securityScanner->scan($this->getFromZipArchive($zip, $relPath)),
921 'SimpleXMLElement',
922 Settings::getLibXmlLoaderOptions()
923 );
924 $vmlCommentsFile->registerXPathNamespace('v', 'urn:schemas-microsoft-com:vml');
925 } catch (\Throwable $ex) {
926 //Ignore unparsable vmlDrawings. Later they will be moved from $unparsedVmlDrawings to $unparsedLoadedData
927 continue;
928 }
929
930 $shapes = $vmlCommentsFile->xpath('//v:shape');
931 foreach ($shapes as $shape) {
932 $shape->registerXPathNamespace('v', 'urn:schemas-microsoft-com:vml');
933
934 if (isset($shape['style'])) {
935 $style = (string) $shape['style'];
936 $fillColor = strtoupper(substr((string) $shape['fillcolor'], 1));
937 $column = null;
938 $row = null;
939
940 $clientData = $shape->xpath('.//x:ClientData');
941 if (is_array($clientData) && !empty($clientData)) {
942 $clientData = $clientData[0];
943
944 if (isset($clientData['ObjectType']) && (string) $clientData['ObjectType'] == 'Note') {
945 $temp = $clientData->xpath('.//x:Row');
946 if (is_array($temp)) {
947 $row = $temp[0];
948 }
949
950 $temp = $clientData->xpath('.//x:Column');
951 if (is_array($temp)) {
952 $column = $temp[0];
953 }
954 }
955 }
956
957 if (($column !== null) && ($row !== null)) {
958 // Set comment properties
959 $comment = $docSheet->getCommentByColumnAndRow($column + 1, $row + 1);
960 $comment->getFillColor()->setRGB($fillColor);
961
962 // Parse style
963 $styleArray = explode(';', str_replace(' ', '', $style));
964 foreach ($styleArray as $stylePair) {
965 $stylePair = explode(':', $stylePair);
966
967 if ($stylePair[0] == 'margin-left') {
968 $comment->setMarginLeft($stylePair[1]);
969 }
970 if ($stylePair[0] == 'margin-top') {
971 $comment->setMarginTop($stylePair[1]);
972 }
973 if ($stylePair[0] == 'width') {
974 $comment->setWidth($stylePair[1]);
975 }
976 if ($stylePair[0] == 'height') {
977 $comment->setHeight($stylePair[1]);
978 }
979 if ($stylePair[0] == 'visibility') {
980 $comment->setVisible($stylePair[1] == 'visible');
981 }
982 }
983
984 unset($unparsedVmlDrawings[$relName]);
985 }
986 }
987 }
988 }
989
990 // unparsed vmlDrawing
991 if ($unparsedVmlDrawings) {
992 foreach ($unparsedVmlDrawings as $rId => $relPath) {
993 $rId = substr($rId, 3); // rIdXXX
994 $unparsedVmlDrawing = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['vmlDrawings'];
995 $unparsedVmlDrawing[$rId] = [];
996 $unparsedVmlDrawing[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $relPath);
997 $unparsedVmlDrawing[$rId]['relFilePath'] = $relPath;
998 $unparsedVmlDrawing[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedVmlDrawing[$rId]['filePath']));
999 unset($unparsedVmlDrawing);
1000 }
1001 }
1002
1003 // Header/footer images
1004 if ($xmlSheet && $xmlSheet->legacyDrawingHF && !$this->readDataOnly) {
1005 if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) {
1006 //~ http://schemas.openxmlformats.org/package/2006/relationships"
1007 $relsWorksheet = simplexml_load_string(
1008 $this->securityScanner->scan(
1009 $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')
1010 ),
1011 'SimpleXMLElement',
1012 Settings::getLibXmlLoaderOptions()
1013 );
1014 $vmlRelationship = '';
1015
1016 foreach ($relsWorksheet->Relationship as $ele) {
1017 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing') {
1018 $vmlRelationship = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
1019 }
1020 }
1021
1022 if ($vmlRelationship != '') {
1023 // Fetch linked images
1024 //~ http://schemas.openxmlformats.org/package/2006/relationships"
1025 $relsVML = simplexml_load_string(
1026 $this->securityScanner->scan(
1027 $this->getFromZipArchive($zip, dirname($vmlRelationship) . '/_rels/' . basename($vmlRelationship) . '.rels')
1028 ),
1029 'SimpleXMLElement',
1030 Settings::getLibXmlLoaderOptions()
1031 );
1032 $drawings = [];
1033 foreach ($relsVML->Relationship as $ele) {
1034 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') {
1035 $drawings[(string) $ele['Id']] = self::dirAdd($vmlRelationship, $ele['Target']);
1036 }
1037 }
1038
1039 // Fetch VML document
1040 $vmlDrawing = simplexml_load_string(
1041 $this->securityScanner->scan($this->getFromZipArchive($zip, $vmlRelationship)),
1042 'SimpleXMLElement',
1043 Settings::getLibXmlLoaderOptions()
1044 );
1045 $vmlDrawing->registerXPathNamespace('v', 'urn:schemas-microsoft-com:vml');
1046
1047 $hfImages = [];
1048
1049 $shapes = $vmlDrawing->xpath('//v:shape');
1050 foreach ($shapes as $idx => $shape) {
1051 $shape->registerXPathNamespace('v', 'urn:schemas-microsoft-com:vml');
1052 $imageData = $shape->xpath('//v:imagedata');
1053
1054 if (!$imageData) {
1055 continue;
1056 }
1057
1058 $imageData = $imageData[$idx];
1059
1060 $imageData = $imageData->attributes('urn:schemas-microsoft-com:office:office');
1061 $style = self::toCSSArray((string) $shape['style']);
1062
1063 $hfImages[(string) $shape['id']] = new HeaderFooterDrawing();
1064 if (isset($imageData['title'])) {
1065 $hfImages[(string) $shape['id']]->setName((string) $imageData['title']);
1066 }
1067
1068 $hfImages[(string) $shape['id']]->setPath('zip://' . File::realpath($pFilename) . '#' . $drawings[(string) $imageData['relid']], false);
1069 $hfImages[(string) $shape['id']]->setResizeProportional(false);
1070 $hfImages[(string) $shape['id']]->setWidth($style['width']);
1071 $hfImages[(string) $shape['id']]->setHeight($style['height']);
1072 if (isset($style['margin-left'])) {
1073 $hfImages[(string) $shape['id']]->setOffsetX($style['margin-left']);
1074 }
1075 $hfImages[(string) $shape['id']]->setOffsetY($style['margin-top']);
1076 $hfImages[(string) $shape['id']]->setResizeProportional(true);
1077 }
1078
1079 $docSheet->getHeaderFooter()->setImages($hfImages);
1080 }
1081 }
1082 }
1083 }
1084
1085 // TODO: Autoshapes from twoCellAnchors!
1086 if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) {
1087 //~ http://schemas.openxmlformats.org/package/2006/relationships"
1088 $relsWorksheet = simplexml_load_string(
1089 $this->securityScanner->scan(
1090 $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')
1091 ),
1092 'SimpleXMLElement',
1093 Settings::getLibXmlLoaderOptions()
1094 );
1095 $drawings = [];
1096 foreach ($relsWorksheet->Relationship as $ele) {
1097 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing') {
1098 $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
1099 }
1100 }
1101 if ($xmlSheet->drawing && !$this->readDataOnly) {
1102 $unparsedDrawings = [];
1103 foreach ($xmlSheet->drawing as $drawing) {
1104 $drawingRelId = (string) self::getArrayItem($drawing->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'id');
1105 $fileDrawing = $drawings[$drawingRelId];
1106 //~ http://schemas.openxmlformats.org/package/2006/relationships"
1107 $relsDrawing = simplexml_load_string(
1108 $this->securityScanner->scan(
1109 $this->getFromZipArchive($zip, dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels')
1110 ),
1111 'SimpleXMLElement',
1112 Settings::getLibXmlLoaderOptions()
1113 );
1114 $images = [];
1115 $hyperlinks = [];
1116 if ($relsDrawing && $relsDrawing->Relationship) {
1117 foreach ($relsDrawing->Relationship as $ele) {
1118 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink') {
1119 $hyperlinks[(string) $ele['Id']] = (string) $ele['Target'];
1120 }
1121 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') {
1122 $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $ele['Target']);
1123 } elseif ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart') {
1124 if ($this->includeCharts) {
1125 $charts[self::dirAdd($fileDrawing, $ele['Target'])] = [
1126 'id' => (string) $ele['Id'],
1127 'sheet' => $docSheet->getTitle(),
1128 ];
1129 }
1130 }
1131 }
1132 }
1133 $xmlDrawing = simplexml_load_string(
1134 $this->securityScanner->scan($this->getFromZipArchive($zip, $fileDrawing)),
1135 'SimpleXMLElement',
1136 Settings::getLibXmlLoaderOptions()
1137 );
1138 $xmlDrawingChildren = $xmlDrawing->children('http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing');
1139
1140 if ($xmlDrawingChildren->oneCellAnchor) {
1141 foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) {
1142 if ($oneCellAnchor->pic->blipFill) {
1143 /** @var SimpleXMLElement $blip */
1144 $blip = $oneCellAnchor->pic->blipFill->children('http://schemas.openxmlformats.org/drawingml/2006/main')->blip;
1145 /** @var SimpleXMLElement $xfrm */
1146 $xfrm = $oneCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->xfrm;
1147 /** @var SimpleXMLElement $outerShdw */
1148 $outerShdw = $oneCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->effectLst->outerShdw;
1149 /** @var \SimpleXMLElement $hlinkClick */
1150 $hlinkClick = $oneCellAnchor->pic->nvPicPr->cNvPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->hlinkClick;
1151
1152 $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
1153 $objDrawing->setName((string) self::getArrayItem($oneCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'name'));
1154 $objDrawing->setDescription((string) self::getArrayItem($oneCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'descr'));
1155 $objDrawing->setPath(
1156 'zip://' . File::realpath($pFilename) . '#' .
1157 $images[(string) self::getArrayItem(
1158 $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'),
1159 'embed'
1160 )],
1161 false
1162 );
1163 $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((string) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1));
1164 $objDrawing->setOffsetX(Drawing::EMUToPixels($oneCellAnchor->from->colOff));
1165 $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff));
1166 $objDrawing->setResizeProportional(false);
1167 $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cx')));
1168 $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cy')));
1169 if ($xfrm) {
1170 $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItem($xfrm->attributes(), 'rot')));
1171 }
1172 if ($outerShdw) {
1173 $shadow = $objDrawing->getShadow();
1174 $shadow->setVisible(true);
1175 $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItem($outerShdw->attributes(), 'blurRad')));
1176 $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItem($outerShdw->attributes(), 'dist')));
1177 $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItem($outerShdw->attributes(), 'dir')));
1178 $shadow->setAlignment((string) self::getArrayItem($outerShdw->attributes(), 'algn'));
1179 $clr = isset($outerShdw->srgbClr) ? $outerShdw->srgbClr : $outerShdw->prstClr;
1180 $shadow->getColor()->setRGB(self::getArrayItem($clr->attributes(), 'val'));
1181 $shadow->setAlpha(self::getArrayItem($clr->alpha->attributes(), 'val') / 1000);
1182 }
1183
1184 $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks);
1185
1186 $objDrawing->setWorksheet($docSheet);
1187 } else {
1188 // ? Can charts be positioned with a oneCellAnchor ?
1189 $coordinates = Coordinate::stringFromColumnIndex(((string) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1);
1190 $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff);
1191 $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff);
1192 $width = Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cx'));
1193 $height = Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cy'));
1194 }
1195 }
1196 }
1197 if ($xmlDrawingChildren->twoCellAnchor) {
1198 foreach ($xmlDrawingChildren->twoCellAnchor as $twoCellAnchor) {
1199 if ($twoCellAnchor->pic->blipFill) {
1200 $blip = $twoCellAnchor->pic->blipFill->children('http://schemas.openxmlformats.org/drawingml/2006/main')->blip;
1201 $xfrm = $twoCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->xfrm;
1202 $outerShdw = $twoCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->effectLst->outerShdw;
1203 $hlinkClick = $twoCellAnchor->pic->nvPicPr->cNvPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->hlinkClick;
1204 $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
1205 $objDrawing->setName((string) self::getArrayItem($twoCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'name'));
1206 $objDrawing->setDescription((string) self::getArrayItem($twoCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'descr'));
1207 $objDrawing->setPath(
1208 'zip://' . File::realpath($pFilename) . '#' .
1209 $images[(string) self::getArrayItem(
1210 $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'),
1211 'embed'
1212 )],
1213 false
1214 );
1215 $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((string) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1));
1216 $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff));
1217 $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff));
1218 $objDrawing->setResizeProportional(false);
1219
1220 if ($xfrm) {
1221 $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem($xfrm->ext->attributes(), 'cx')));
1222 $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem($xfrm->ext->attributes(), 'cy')));
1223 $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItem($xfrm->attributes(), 'rot')));
1224 }
1225 if ($outerShdw) {
1226 $shadow = $objDrawing->getShadow();
1227 $shadow->setVisible(true);
1228 $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItem($outerShdw->attributes(), 'blurRad')));
1229 $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItem($outerShdw->attributes(), 'dist')));
1230 $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItem($outerShdw->attributes(), 'dir')));
1231 $shadow->setAlignment((string) self::getArrayItem($outerShdw->attributes(), 'algn'));
1232 $clr = isset($outerShdw->srgbClr) ? $outerShdw->srgbClr : $outerShdw->prstClr;
1233 $shadow->getColor()->setRGB(self::getArrayItem($clr->attributes(), 'val'));
1234 $shadow->setAlpha(self::getArrayItem($clr->alpha->attributes(), 'val') / 1000);
1235 }
1236
1237 $this->readHyperLinkDrawing($objDrawing, $twoCellAnchor, $hyperlinks);
1238
1239 $objDrawing->setWorksheet($docSheet);
1240 } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) {
1241 $fromCoordinate = Coordinate::stringFromColumnIndex(((string) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1);
1242 $fromOffsetX = Drawing::EMUToPixels($twoCellAnchor->from->colOff);
1243 $fromOffsetY = Drawing::EMUToPixels($twoCellAnchor->from->rowOff);
1244 $toCoordinate = Coordinate::stringFromColumnIndex(((string) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1);
1245 $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff);
1246 $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff);
1247 $graphic = $twoCellAnchor->graphicFrame->children('http://schemas.openxmlformats.org/drawingml/2006/main')->graphic;
1248 /** @var SimpleXMLElement $chartRef */
1249 $chartRef = $graphic->graphicData->children('http://schemas.openxmlformats.org/drawingml/2006/chart')->chart;
1250 $thisChart = (string) $chartRef->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships');
1251
1252 $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1253 'fromCoordinate' => $fromCoordinate,
1254 'fromOffsetX' => $fromOffsetX,
1255 'fromOffsetY' => $fromOffsetY,
1256 'toCoordinate' => $toCoordinate,
1257 'toOffsetX' => $toOffsetX,
1258 'toOffsetY' => $toOffsetY,
1259 'worksheetTitle' => $docSheet->getTitle(),
1260 ];
1261 }
1262 }
1263 }
1264 if ($relsDrawing === false && $xmlDrawing->count() == 0) {
1265 // Save Drawing without rels and children as unparsed
1266 $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML();
1267 }
1268 }
1269
1270 // store original rId of drawing files
1271 $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = [];
1272 foreach ($relsWorksheet->Relationship as $ele) {
1273 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing') {
1274 $drawingRelId = (string) $ele['Id'];
1275 $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = $drawingRelId;
1276 if (isset($unparsedDrawings[$drawingRelId])) {
1277 $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['Drawings'][$drawingRelId] = $unparsedDrawings[$drawingRelId];
1278 }
1279 }
1280 }
1281
1282 // unparsed drawing AlternateContent
1283 $xmlAltDrawing = simplexml_load_string(
1284 $this->securityScanner->scan($this->getFromZipArchive($zip, $fileDrawing)),
1285 'SimpleXMLElement',
1286 Settings::getLibXmlLoaderOptions()
1287 )->children('http://schemas.openxmlformats.org/markup-compatibility/2006');
1288
1289 if ($xmlAltDrawing->AlternateContent) {
1290 foreach ($xmlAltDrawing->AlternateContent as $alternateContent) {
1291 $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingAlternateContents'][] = $alternateContent->asXML();
1292 }
1293 }
1294 }
1295 }
1296
1297 $this->readFormControlProperties($excel, $zip, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
1298 $this->readPrinterSettings($excel, $zip, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
1299
1300 // Loop through definedNames
1301 if ($xmlWorkbook->definedNames) {
1302 foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
1303 // Extract range
1304 $extractedRange = (string) $definedName;
1305 if (($spos = strpos($extractedRange, '!')) !== false) {
1306 $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos));
1307 } else {
1308 $extractedRange = str_replace('$', '', $extractedRange);
1309 }
1310
1311 // Valid range?
1312 if (stripos((string) $definedName, '#REF!') !== false || $extractedRange == '') {
1313 continue;
1314 }
1315
1316 // Some definedNames are only applicable if we are on the same sheet...
1317 if ((string) $definedName['localSheetId'] != '' && (string) $definedName['localSheetId'] == $oldSheetId) {
1318 // Switch on type
1319 switch ((string) $definedName['name']) {
1320 case '_xlnm._FilterDatabase':
1321 if ((string) $definedName['hidden'] !== '1') {
1322 $extractedRange = explode(',', $extractedRange);
1323 foreach ($extractedRange as $range) {
1324 $autoFilterRange = $range;
1325 if (strpos($autoFilterRange, ':') !== false) {
1326 $docSheet->getAutoFilter()->setRange($autoFilterRange);
1327 }
1328 }
1329 }
1330
1331 break;
1332 case '_xlnm.Print_Titles':
1333 // Split $extractedRange
1334 $extractedRange = explode(',', $extractedRange);
1335
1336 // Set print titles
1337 foreach ($extractedRange as $range) {
1338 $matches = [];
1339 $range = str_replace('$', '', $range);
1340
1341 // check for repeating columns, e g. 'A:A' or 'A:D'
1342 if (preg_match('/!?([A-Z]+)\:([A-Z]+)$/', $range, $matches)) {
1343 $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$matches[1], $matches[2]]);
1344 } elseif (preg_match('/!?(\d+)\:(\d+)$/', $range, $matches)) {
1345 // check for repeating rows, e.g. '1:1' or '1:5'
1346 $docSheet->getPageSetup()->setRowsToRepeatAtTop([$matches[1], $matches[2]]);
1347 }
1348 }
1349
1350 break;
1351 case '_xlnm.Print_Area':
1352 $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
1353 $newRangeSets = [];
1354 foreach ($rangeSets as $rangeSet) {
1355 list($sheetName, $rangeSet) = Worksheet::extractSheetTitle($rangeSet, true);
1356 if (strpos($rangeSet, ':') === false) {
1357 $rangeSet = $rangeSet . ':' . $rangeSet;
1358 }
1359 $newRangeSets[] = str_replace('$', '', $rangeSet);
1360 }
1361 $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets));
1362
1363 break;
1364 default:
1365 break;
1366 }
1367 }
1368 }
1369 }
1370
1371 // Next sheet id
1372 ++$sheetId;
1373 }
1374
1375 // Loop through definedNames
1376 if ($xmlWorkbook->definedNames) {
1377 foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
1378 // Extract range
1379 $extractedRange = (string) $definedName;
1380 if (($spos = strpos($extractedRange, '!')) !== false) {
1381 $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos));
1382 } else {
1383 $extractedRange = str_replace('$', '', $extractedRange);
1384 }
1385
1386 // Valid range?
1387 if (stripos((string) $definedName, '#REF!') !== false || $extractedRange == '') {
1388 continue;
1389 }
1390
1391 // Some definedNames are only applicable if we are on the same sheet...
1392 if ((string) $definedName['localSheetId'] != '') {
1393 // Local defined name
1394 // Switch on type
1395 switch ((string) $definedName['name']) {
1396 case '_xlnm._FilterDatabase':
1397 case '_xlnm.Print_Titles':
1398 case '_xlnm.Print_Area':
1399 break;
1400 default:
1401 if ($mapSheetId[(int) $definedName['localSheetId']] !== null) {
1402 if (strpos((string) $definedName, '!') !== false) {
1403 $range = Worksheet::extractSheetTitle((string) $definedName, true);
1404 $range[0] = str_replace("''", "'", $range[0]);
1405 $range[0] = str_replace("'", '', $range[0]);
1406 if ($worksheet = $docSheet->getParent()->getSheetByName($range[0])) {
1407 $extractedRange = str_replace('$', '', $range[1]);
1408 $scope = $docSheet->getParent()->getSheet($mapSheetId[(int) $definedName['localSheetId']]);
1409 $excel->addNamedRange(new NamedRange((string) $definedName['name'], $worksheet, $extractedRange, true, $scope));
1410 }
1411 }
1412 }
1413
1414 break;
1415 }
1416 } elseif (!isset($definedName['localSheetId'])) {
1417 // "Global" definedNames
1418 $locatedSheet = null;
1419 $extractedSheetName = '';
1420 if (strpos((string) $definedName, '!') !== false) {
1421 // Extract sheet name
1422 $extractedSheetName = Worksheet::extractSheetTitle((string) $definedName, true);
1423 $extractedSheetName = trim($extractedSheetName[0], "'");
1424
1425 // Locate sheet
1426 $locatedSheet = $excel->getSheetByName($extractedSheetName);
1427
1428 // Modify range
1429 list($worksheetName, $extractedRange) = Worksheet::extractSheetTitle($extractedRange, true);
1430 }
1431
1432 if ($locatedSheet !== null) {
1433 $excel->addNamedRange(new NamedRange((string) $definedName['name'], $locatedSheet, $extractedRange, false));
1434 }
1435 }
1436 }
1437 }
1438 }
1439
1440 if ((!$this->readDataOnly) || (!empty($this->loadSheetsOnly))) {
1441 $workbookView = $xmlWorkbook->bookViews->workbookView;
1442
1443 // active sheet index
1444 $activeTab = (int) ($workbookView['activeTab']); // refers to old sheet index
1445
1446 // keep active sheet index if sheet is still loaded, else first sheet is set as the active
1447 if (isset($mapSheetId[$activeTab]) && $mapSheetId[$activeTab] !== null) {
1448 $excel->setActiveSheetIndex($mapSheetId[$activeTab]);
1449 } else {
1450 if ($excel->getSheetCount() == 0) {
1451 $excel->createSheet();
1452 }
1453 $excel->setActiveSheetIndex(0);
1454 }
1455
1456 if (isset($workbookView['showHorizontalScroll'])) {
1457 $showHorizontalScroll = (string) $workbookView['showHorizontalScroll'];
1458 $excel->setShowHorizontalScroll($this->castXsdBooleanToBool($showHorizontalScroll));
1459 }
1460
1461 if (isset($workbookView['showVerticalScroll'])) {
1462 $showVerticalScroll = (string) $workbookView['showVerticalScroll'];
1463 $excel->setShowVerticalScroll($this->castXsdBooleanToBool($showVerticalScroll));
1464 }
1465
1466 if (isset($workbookView['showSheetTabs'])) {
1467 $showSheetTabs = (string) $workbookView['showSheetTabs'];
1468 $excel->setShowSheetTabs($this->castXsdBooleanToBool($showSheetTabs));
1469 }
1470
1471 if (isset($workbookView['minimized'])) {
1472 $minimized = (string) $workbookView['minimized'];
1473 $excel->setMinimized($this->castXsdBooleanToBool($minimized));
1474 }
1475
1476 if (isset($workbookView['autoFilterDateGrouping'])) {
1477 $autoFilterDateGrouping = (string) $workbookView['autoFilterDateGrouping'];
1478 $excel->setAutoFilterDateGrouping($this->castXsdBooleanToBool($autoFilterDateGrouping));
1479 }
1480
1481 if (isset($workbookView['firstSheet'])) {
1482 $firstSheet = (string) $workbookView['firstSheet'];
1483 $excel->setFirstSheetIndex((int) $firstSheet);
1484 }
1485
1486 if (isset($workbookView['visibility'])) {
1487 $visibility = (string) $workbookView['visibility'];
1488 $excel->setVisibility($visibility);
1489 }
1490
1491 if (isset($workbookView['tabRatio'])) {
1492 $tabRatio = (string) $workbookView['tabRatio'];
1493 $excel->setTabRatio((int) $tabRatio);
1494 }
1495 }
1496
1497 break;
1498 }
1499 }
1500
1501 if (!$this->readDataOnly) {
1502 $contentTypes = simplexml_load_string(
1503 $this->securityScanner->scan(
1504 $this->getFromZipArchive($zip, '[Content_Types].xml')
1505 ),
1506 'SimpleXMLElement',
1507 Settings::getLibXmlLoaderOptions()
1508 );
1509
1510 // Default content types
1511 foreach ($contentTypes->Default as $contentType) {
1512 switch ($contentType['ContentType']) {
1513 case 'application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings':
1514 $unparsedLoadedData['default_content_types'][(string) $contentType['Extension']] = (string) $contentType['ContentType'];
1515
1516 break;
1517 }
1518 }
1519
1520 // Override content types
1521 foreach ($contentTypes->Override as $contentType) {
1522 switch ($contentType['ContentType']) {
1523 case 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml':
1524 if ($this->includeCharts) {
1525 $chartEntryRef = ltrim($contentType['PartName'], '/');
1526 $chartElements = simplexml_load_string(
1527 $this->securityScanner->scan(
1528 $this->getFromZipArchive($zip, $chartEntryRef)
1529 ),
1530 'SimpleXMLElement',
1531 Settings::getLibXmlLoaderOptions()
1532 );
1533 $objChart = Chart::readChart($chartElements, basename($chartEntryRef, '.xml'));
1534
1535 if (isset($charts[$chartEntryRef])) {
1536 $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id'];
1537 if (isset($chartDetails[$chartPositionRef])) {
1538 $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart);
1539 $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet']));
1540 $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
1541 $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']);
1542 }
1543 }
1544 }
1545
1546 break;
1547
1548 // unparsed
1549 case 'application/vnd.ms-excel.controlproperties+xml':
1550 $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType'];
1551
1552 break;
1553 }
1554 }
1555 }
1556
1557 $excel->setUnparsedLoadedData($unparsedLoadedData);
1558
1559 $zip->close();
1560
1561 return $excel;
1562 }
1563
1564 private static function readColor($color, $background = false)
1565 {
1566 if (isset($color['rgb'])) {
1567 return (string) $color['rgb'];
1568 } elseif (isset($color['indexed'])) {
1569 return Color::indexedColor($color['indexed'] - 7, $background)->getARGB();
1570 } elseif (isset($color['theme'])) {
1571 if (self::$theme !== null) {
1572 $returnColour = self::$theme->getColourByIndex((int) $color['theme']);
1573 if (isset($color['tint'])) {
1574 $tintAdjust = (float) $color['tint'];
1575 $returnColour = Color::changeBrightness($returnColour, $tintAdjust);
1576 }
1577
1578 return 'FF' . $returnColour;
1579 }
1580 }
1581
1582 if ($background) {
1583 return 'FFFFFFFF';
1584 }
1585
1586 return 'FF000000';
1587 }
1588
1589 /**
1590 * @param Style $docStyle
1591 * @param SimpleXMLElement|\stdClass $style
1592 */
1593 private static function readStyle(Style $docStyle, $style)
1594 {
1595 $docStyle->getNumberFormat()->setFormatCode($style->numFmt);
1596
1597 // font
1598 if (isset($style->font)) {
1599 $docStyle->getFont()->setName((string) $style->font->name['val']);
1600 $docStyle->getFont()->setSize((string) $style->font->sz['val']);
1601 if (isset($style->font->b)) {
1602 $docStyle->getFont()->setBold(!isset($style->font->b['val']) || self::boolean((string) $style->font->b['val']));
1603 }
1604 if (isset($style->font->i)) {
1605 $docStyle->getFont()->setItalic(!isset($style->font->i['val']) || self::boolean((string) $style->font->i['val']));
1606 }
1607 if (isset($style->font->strike)) {
1608 $docStyle->getFont()->setStrikethrough(!isset($style->font->strike['val']) || self::boolean((string) $style->font->strike['val']));
1609 }
1610 $docStyle->getFont()->getColor()->setARGB(self::readColor($style->font->color));
1611
1612 if (isset($style->font->u) && !isset($style->font->u['val'])) {
1613 $docStyle->getFont()->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE);
1614 } elseif (isset($style->font->u, $style->font->u['val'])) {
1615 $docStyle->getFont()->setUnderline((string) $style->font->u['val']);
1616 }
1617
1618 if (isset($style->font->vertAlign, $style->font->vertAlign['val'])) {
1619 $vertAlign = strtolower((string) $style->font->vertAlign['val']);
1620 if ($vertAlign == 'superscript') {
1621 $docStyle->getFont()->setSuperscript(true);
1622 }
1623 if ($vertAlign == 'subscript') {
1624 $docStyle->getFont()->setSubscript(true);
1625 }
1626 }
1627 }
1628
1629 // fill
1630 if (isset($style->fill)) {
1631 if ($style->fill->gradientFill) {
1632 /** @var SimpleXMLElement $gradientFill */
1633 $gradientFill = $style->fill->gradientFill[0];
1634 if (!empty($gradientFill['type'])) {
1635 $docStyle->getFill()->setFillType((string) $gradientFill['type']);
1636 }
1637 $docStyle->getFill()->setRotation((float) ($gradientFill['degree']));
1638 $gradientFill->registerXPathNamespace('sml', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
1639 $docStyle->getFill()->getStartColor()->setARGB(self::readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color));
1640 $docStyle->getFill()->getEndColor()->setARGB(self::readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color));
1641 } elseif ($style->fill->patternFill) {
1642 $patternType = (string) $style->fill->patternFill['patternType'] != '' ? (string) $style->fill->patternFill['patternType'] : 'solid';
1643 $docStyle->getFill()->setFillType($patternType);
1644 if ($style->fill->patternFill->fgColor) {
1645 $docStyle->getFill()->getStartColor()->setARGB(self::readColor($style->fill->patternFill->fgColor, true));
1646 } else {
1647 $docStyle->getFill()->getStartColor()->setARGB('FF000000');
1648 }
1649 if ($style->fill->patternFill->bgColor) {
1650 $docStyle->getFill()->getEndColor()->setARGB(self::readColor($style->fill->patternFill->bgColor, true));
1651 }
1652 }
1653 }
1654
1655 // border
1656 if (isset($style->border)) {
1657 $diagonalUp = self::boolean((string) $style->border['diagonalUp']);
1658 $diagonalDown = self::boolean((string) $style->border['diagonalDown']);
1659 if (!$diagonalUp && !$diagonalDown) {
1660 $docStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
1661 } elseif ($diagonalUp && !$diagonalDown) {
1662 $docStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
1663 } elseif (!$diagonalUp && $diagonalDown) {
1664 $docStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
1665 } else {
1666 $docStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
1667 }
1668 self::readBorder($docStyle->getBorders()->getLeft(), $style->border->left);
1669 self::readBorder($docStyle->getBorders()->getRight(), $style->border->right);
1670 self::readBorder($docStyle->getBorders()->getTop(), $style->border->top);
1671 self::readBorder($docStyle->getBorders()->getBottom(), $style->border->bottom);
1672 self::readBorder($docStyle->getBorders()->getDiagonal(), $style->border->diagonal);
1673 }
1674
1675 // alignment
1676 if (isset($style->alignment)) {
1677 $docStyle->getAlignment()->setHorizontal((string) $style->alignment['horizontal']);
1678 $docStyle->getAlignment()->setVertical((string) $style->alignment['vertical']);
1679
1680 $textRotation = 0;
1681 if ((int) $style->alignment['textRotation'] <= 90) {
1682 $textRotation = (int) $style->alignment['textRotation'];
1683 } elseif ((int) $style->alignment['textRotation'] > 90) {
1684 $textRotation = 90 - (int) $style->alignment['textRotation'];
1685 }
1686
1687 $docStyle->getAlignment()->setTextRotation((int) $textRotation);
1688 $docStyle->getAlignment()->setWrapText(self::boolean((string) $style->alignment['wrapText']));
1689 $docStyle->getAlignment()->setShrinkToFit(self::boolean((string) $style->alignment['shrinkToFit']));
1690 $docStyle->getAlignment()->setIndent((int) ((string) $style->alignment['indent']) > 0 ? (int) ((string) $style->alignment['indent']) : 0);
1691 $docStyle->getAlignment()->setReadOrder((int) ((string) $style->alignment['readingOrder']) > 0 ? (int) ((string) $style->alignment['readingOrder']) : 0);
1692 }
1693
1694 // protection
1695 if (isset($style->protection)) {
1696 if (isset($style->protection['locked'])) {
1697 if (self::boolean((string) $style->protection['locked'])) {
1698 $docStyle->getProtection()->setLocked(Protection::PROTECTION_PROTECTED);
1699 } else {
1700 $docStyle->getProtection()->setLocked(Protection::PROTECTION_UNPROTECTED);
1701 }
1702 }
1703
1704 if (isset($style->protection['hidden'])) {
1705 if (self::boolean((string) $style->protection['hidden'])) {
1706 $docStyle->getProtection()->setHidden(Protection::PROTECTION_PROTECTED);
1707 } else {
1708 $docStyle->getProtection()->setHidden(Protection::PROTECTION_UNPROTECTED);
1709 }
1710 }
1711 }
1712
1713 // top-level style settings
1714 if (isset($style->quotePrefix)) {
1715 $docStyle->setQuotePrefix($style->quotePrefix);
1716 }
1717 }
1718
1719 /**
1720 * @param Border $docBorder
1721 * @param SimpleXMLElement $eleBorder
1722 */
1723 private static function readBorder(Border $docBorder, $eleBorder)
1724 {
1725 if (isset($eleBorder['style'])) {
1726 $docBorder->setBorderStyle((string) $eleBorder['style']);
1727 }
1728 if (isset($eleBorder->color)) {
1729 $docBorder->getColor()->setARGB(self::readColor($eleBorder->color));
1730 }
1731 }
1732
1733 /**
1734 * @param SimpleXMLElement | null $is
1735 *
1736 * @return RichText
1737 */
1738 private function parseRichText($is)
1739 {
1740 $value = new RichText();
1741
1742 if (isset($is->t)) {
1743 $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t));
1744 } else {
1745 if (is_object($is->r)) {
1746 foreach ($is->r as $run) {
1747 if (!isset($run->rPr)) {
1748 $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
1749 } else {
1750 $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
1751
1752 if (isset($run->rPr->rFont['val'])) {
1753 $objText->getFont()->setName((string) $run->rPr->rFont['val']);
1754 }
1755 if (isset($run->rPr->sz['val'])) {
1756 $objText->getFont()->setSize((float) $run->rPr->sz['val']);
1757 }
1758 if (isset($run->rPr->color)) {
1759 $objText->getFont()->setColor(new Color(self::readColor($run->rPr->color)));
1760 }
1761 if ((isset($run->rPr->b['val']) && self::boolean((string) $run->rPr->b['val'])) ||
1762 (isset($run->rPr->b) && !isset($run->rPr->b['val']))) {
1763 $objText->getFont()->setBold(true);
1764 }
1765 if ((isset($run->rPr->i['val']) && self::boolean((string) $run->rPr->i['val'])) ||
1766 (isset($run->rPr->i) && !isset($run->rPr->i['val']))) {
1767 $objText->getFont()->setItalic(true);
1768 }
1769 if (isset($run->rPr->vertAlign, $run->rPr->vertAlign['val'])) {
1770 $vertAlign = strtolower((string) $run->rPr->vertAlign['val']);
1771 if ($vertAlign == 'superscript') {
1772 $objText->getFont()->setSuperscript(true);
1773 }
1774 if ($vertAlign == 'subscript') {
1775 $objText->getFont()->setSubscript(true);
1776 }
1777 }
1778 if (isset($run->rPr->u) && !isset($run->rPr->u['val'])) {
1779 $objText->getFont()->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE);
1780 } elseif (isset($run->rPr->u, $run->rPr->u['val'])) {
1781 $objText->getFont()->setUnderline((string) $run->rPr->u['val']);
1782 }
1783 if ((isset($run->rPr->strike['val']) && self::boolean((string) $run->rPr->strike['val'])) ||
1784 (isset($run->rPr->strike) && !isset($run->rPr->strike['val']))) {
1785 $objText->getFont()->setStrikethrough(true);
1786 }
1787 }
1788 }
1789 }
1790 }
1791
1792 return $value;
1793 }
1794
1795 /**
1796 * @param Spreadsheet $excel
1797 * @param mixed $customUITarget
1798 * @param mixed $zip
1799 */
1800 private function readRibbon(Spreadsheet $excel, $customUITarget, $zip)
1801 {
1802 $baseDir = dirname($customUITarget);
1803 $nameCustomUI = basename($customUITarget);
1804 // get the xml file (ribbon)
1805 $localRibbon = $this->getFromZipArchive($zip, $customUITarget);
1806 $customUIImagesNames = [];
1807 $customUIImagesBinaries = [];
1808 // something like customUI/_rels/customUI.xml.rels
1809 $pathRels = $baseDir . '/_rels/' . $nameCustomUI . '.rels';
1810 $dataRels = $this->getFromZipArchive($zip, $pathRels);
1811 if ($dataRels) {
1812 // exists and not empty if the ribbon have some pictures (other than internal MSO)
1813 $UIRels = simplexml_load_string(
1814 $this->securityScanner->scan($dataRels),
1815 'SimpleXMLElement',
1816 Settings::getLibXmlLoaderOptions()
1817 );
1818 if (false !== $UIRels) {
1819 // we need to save id and target to avoid parsing customUI.xml and "guess" if it's a pseudo callback who load the image
1820 foreach ($UIRels->Relationship as $ele) {
1821 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') {
1822 // an image ?
1823 $customUIImagesNames[(string) $ele['Id']] = (string) $ele['Target'];
1824 $customUIImagesBinaries[(string) $ele['Target']] = $this->getFromZipArchive($zip, $baseDir . '/' . (string) $ele['Target']);
1825 }
1826 }
1827 }
1828 }
1829 if ($localRibbon) {
1830 $excel->setRibbonXMLData($customUITarget, $localRibbon);
1831 if (count($customUIImagesNames) > 0 && count($customUIImagesBinaries) > 0) {
1832 $excel->setRibbonBinObjects($customUIImagesNames, $customUIImagesBinaries);
1833 } else {
1834 $excel->setRibbonBinObjects(null, null);
1835 }
1836 } else {
1837 $excel->setRibbonXMLData(null, null);
1838 $excel->setRibbonBinObjects(null, null);
1839 }
1840 }
1841
1842 private static function getArrayItem($array, $key = 0)
1843 {
1844 return isset($array[$key]) ? $array[$key] : null;
1845 }
1846
1847 private static function dirAdd($base, $add)
1848 {
1849 return preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add");
1850 }
1851
1852 private static function toCSSArray($style)
1853 {
1854 $style = trim(str_replace(["\r", "\n"], '', $style), ';');
1855
1856 $temp = explode(';', $style);
1857 $style = [];
1858 foreach ($temp as $item) {
1859 $item = explode(':', $item);
1860
1861 if (strpos($item[1], 'px') !== false) {
1862 $item[1] = str_replace('px', '', $item[1]);
1863 }
1864 if (strpos($item[1], 'pt') !== false) {
1865 $item[1] = str_replace('pt', '', $item[1]);
1866 $item[1] = Font::fontSizeToPixels($item[1]);
1867 }
1868 if (strpos($item[1], 'in') !== false) {
1869 $item[1] = str_replace('in', '', $item[1]);
1870 $item[1] = Font::inchSizeToPixels($item[1]);
1871 }
1872 if (strpos($item[1], 'cm') !== false) {
1873 $item[1] = str_replace('cm', '', $item[1]);
1874 $item[1] = Font::centimeterSizeToPixels($item[1]);
1875 }
1876
1877 $style[$item[0]] = $item[1];
1878 }
1879
1880 return $style;
1881 }
1882
1883 private static function boolean($value)
1884 {
1885 if (is_object($value)) {
1886 $value = (string) $value;
1887 }
1888 if (is_numeric($value)) {
1889 return (bool) $value;
1890 }
1891
1892 return $value === 'true' || $value === 'TRUE';
1893 }
1894
1895 /**
1896 * @param \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $objDrawing
1897 * @param \SimpleXMLElement $cellAnchor
1898 * @param array $hyperlinks
1899 */
1900 private function readHyperLinkDrawing($objDrawing, $cellAnchor, $hyperlinks)
1901 {
1902 $hlinkClick = $cellAnchor->pic->nvPicPr->cNvPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->hlinkClick;
1903
1904 if ($hlinkClick->count() === 0) {
1905 return;
1906 }
1907
1908 $hlinkId = (string) $hlinkClick->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships')['id'];
1909 $hyperlink = new Hyperlink(
1910 $hyperlinks[$hlinkId],
1911 (string) self::getArrayItem($cellAnchor->pic->nvPicPr->cNvPr->attributes(), 'name')
1912 );
1913 $objDrawing->setHyperlink($hyperlink);
1914 }
1915
1916 private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkbook)
1917 {
1918 if (!$xmlWorkbook->workbookProtection) {
1919 return;
1920 }
1921
1922 if ($xmlWorkbook->workbookProtection['lockRevision']) {
1923 $excel->getSecurity()->setLockRevision((bool) $xmlWorkbook->workbookProtection['lockRevision']);
1924 }
1925
1926 if ($xmlWorkbook->workbookProtection['lockStructure']) {
1927 $excel->getSecurity()->setLockStructure((bool) $xmlWorkbook->workbookProtection['lockStructure']);
1928 }
1929
1930 if ($xmlWorkbook->workbookProtection['lockWindows']) {
1931 $excel->getSecurity()->setLockWindows((bool) $xmlWorkbook->workbookProtection['lockWindows']);
1932 }
1933
1934 if ($xmlWorkbook->workbookProtection['revisionsPassword']) {
1935 $excel->getSecurity()->setRevisionsPassword((string) $xmlWorkbook->workbookProtection['revisionsPassword'], true);
1936 }
1937
1938 if ($xmlWorkbook->workbookProtection['workbookPassword']) {
1939 $excel->getSecurity()->setWorkbookPassword((string) $xmlWorkbook->workbookProtection['workbookPassword'], true);
1940 }
1941 }
1942
1943 private function readFormControlProperties(Spreadsheet $excel, ZipArchive $zip, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData)
1944 {
1945 if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) {
1946 return;
1947 }
1948
1949 //~ http://schemas.openxmlformats.org/package/2006/relationships"
1950 $relsWorksheet = simplexml_load_string(
1951 $this->securityScanner->scan(
1952 $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')
1953 ),
1954 'SimpleXMLElement',
1955 Settings::getLibXmlLoaderOptions()
1956 );
1957 $ctrlProps = [];
1958 foreach ($relsWorksheet->Relationship as $ele) {
1959 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/ctrlProp') {
1960 $ctrlProps[(string) $ele['Id']] = $ele;
1961 }
1962 }
1963
1964 $unparsedCtrlProps = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['ctrlProps'];
1965 foreach ($ctrlProps as $rId => $ctrlProp) {
1966 $rId = substr($rId, 3); // rIdXXX
1967 $unparsedCtrlProps[$rId] = [];
1968 $unparsedCtrlProps[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $ctrlProp['Target']);
1969 $unparsedCtrlProps[$rId]['relFilePath'] = (string) $ctrlProp['Target'];
1970 $unparsedCtrlProps[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedCtrlProps[$rId]['filePath']));
1971 }
1972 unset($unparsedCtrlProps);
1973 }
1974
1975 private function readPrinterSettings(Spreadsheet $excel, ZipArchive $zip, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData)
1976 {
1977 if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) {
1978 return;
1979 }
1980
1981 //~ http://schemas.openxmlformats.org/package/2006/relationships"
1982 $relsWorksheet = simplexml_load_string(
1983 $this->securityScanner->scan(
1984 $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')
1985 ),
1986 'SimpleXMLElement',
1987 Settings::getLibXmlLoaderOptions()
1988 );
1989 $sheetPrinterSettings = [];
1990 foreach ($relsWorksheet->Relationship as $ele) {
1991 if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings') {
1992 $sheetPrinterSettings[(string) $ele['Id']] = $ele;
1993 }
1994 }
1995
1996 $unparsedPrinterSettings = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['printerSettings'];
1997 foreach ($sheetPrinterSettings as $rId => $printerSettings) {
1998 $rId = substr($rId, 3); // rIdXXX
1999 $unparsedPrinterSettings[$rId] = [];
2000 $unparsedPrinterSettings[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $printerSettings['Target']);
2001 $unparsedPrinterSettings[$rId]['relFilePath'] = (string) $printerSettings['Target'];
2002 $unparsedPrinterSettings[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedPrinterSettings[$rId]['filePath']));
2003 }
2004 unset($unparsedPrinterSettings);
2005 }
2006
2007 /**
2008 * Convert an 'xsd:boolean' XML value to a PHP boolean value.
2009 * A valid 'xsd:boolean' XML value can be one of the following
2010 * four values: 'true', 'false', '1', '0'. It is case sensitive.
2011 *
2012 * Note that just doing '(bool) $xsdBoolean' is not safe,
2013 * since '(bool) "false"' returns true.
2014 *
2015 * @see https://www.w3.org/TR/xmlschema11-2/#boolean
2016 *
2017 * @param string $xsdBoolean An XML string value of type 'xsd:boolean'
2018 *
2019 * @return bool Boolean value
2020 */
2021 private function castXsdBooleanToBool($xsdBoolean)
2022 {
2023 if ($xsdBoolean === 'false') {
2024 return false;
2025 }
2026
2027 return (bool) $xsdBoolean;
2028 }
2029 }
2030