1<?php 2 3namespace PhpOffice\PhpSpreadsheet\Reader\Xls; 4 5use PhpOffice\PhpSpreadsheet\Cell\Coordinate; 6use PhpOffice\PhpSpreadsheet\Reader\Xls; 7use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer; 8use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer; 9use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer; 10use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer; 11use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer; 12use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE; 13use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE\Blip; 14 15class Escher 16{ 17 const DGGCONTAINER = 0xF000; 18 const BSTORECONTAINER = 0xF001; 19 const DGCONTAINER = 0xF002; 20 const SPGRCONTAINER = 0xF003; 21 const SPCONTAINER = 0xF004; 22 const DGG = 0xF006; 23 const BSE = 0xF007; 24 const DG = 0xF008; 25 const SPGR = 0xF009; 26 const SP = 0xF00A; 27 const OPT = 0xF00B; 28 const CLIENTTEXTBOX = 0xF00D; 29 const CLIENTANCHOR = 0xF010; 30 const CLIENTDATA = 0xF011; 31 const BLIPJPEG = 0xF01D; 32 const BLIPPNG = 0xF01E; 33 const SPLITMENUCOLORS = 0xF11E; 34 const TERTIARYOPT = 0xF122; 35 36 /** 37 * Escher stream data (binary). 38 * 39 * @var string 40 */ 41 private $data; 42 43 /** 44 * Size in bytes of the Escher stream data. 45 * 46 * @var int 47 */ 48 private $dataSize; 49 50 /** 51 * Current position of stream pointer in Escher stream data. 52 * 53 * @var int 54 */ 55 private $pos; 56 57 /** 58 * The object to be returned by the reader. Modified during load. 59 * 60 * @var BSE|BstoreContainer|DgContainer|DggContainer|\PhpOffice\PhpSpreadsheet\Shared\Escher|SpContainer|SpgrContainer 61 */ 62 private $object; 63 64 /** 65 * Create a new Escher instance. 66 * 67 * @param mixed $object 68 */ 69 public function __construct($object) 70 { 71 $this->object = $object; 72 } 73 74 /** 75 * Load Escher stream data. May be a partial Escher stream. 76 * 77 * @param string $data 78 * 79 * @return BSE|BstoreContainer|DgContainer|DggContainer|\PhpOffice\PhpSpreadsheet\Shared\Escher|SpContainer|SpgrContainer 80 */ 81 public function load($data) 82 { 83 $this->data = $data; 84 85 // total byte size of Excel data (workbook global substream + sheet substreams) 86 $this->dataSize = strlen($this->data); 87 88 $this->pos = 0; 89 90 // Parse Escher stream 91 while ($this->pos < $this->dataSize) { 92 // offset: 2; size: 2: Record Type 93 $fbt = Xls::getUInt2d($this->data, $this->pos + 2); 94 95 switch ($fbt) { 96 case self::DGGCONTAINER: 97 $this->readDggContainer(); 98 99 break; 100 case self::DGG: 101 $this->readDgg(); 102 103 break; 104 case self::BSTORECONTAINER: 105 $this->readBstoreContainer(); 106 107 break; 108 case self::BSE: 109 $this->readBSE(); 110 111 break; 112 case self::BLIPJPEG: 113 $this->readBlipJPEG(); 114 115 break; 116 case self::BLIPPNG: 117 $this->readBlipPNG(); 118 119 break; 120 case self::OPT: 121 $this->readOPT(); 122 123 break; 124 case self::TERTIARYOPT: 125 $this->readTertiaryOPT(); 126 127 break; 128 case self::SPLITMENUCOLORS: 129 $this->readSplitMenuColors(); 130 131 break; 132 case self::DGCONTAINER: 133 $this->readDgContainer(); 134 135 break; 136 case self::DG: 137 $this->readDg(); 138 139 break; 140 case self::SPGRCONTAINER: 141 $this->readSpgrContainer(); 142 143 break; 144 case self::SPCONTAINER: 145 $this->readSpContainer(); 146 147 break; 148 case self::SPGR: 149 $this->readSpgr(); 150 151 break; 152 case self::SP: 153 $this->readSp(); 154 155 break; 156 case self::CLIENTTEXTBOX: 157 $this->readClientTextbox(); 158 159 break; 160 case self::CLIENTANCHOR: 161 $this->readClientAnchor(); 162 163 break; 164 case self::CLIENTDATA: 165 $this->readClientData(); 166 167 break; 168 default: 169 $this->readDefault(); 170 171 break; 172 } 173 } 174 175 return $this->object; 176 } 177 178 /** 179 * Read a generic record. 180 */ 181 private function readDefault() 182 { 183 // offset 0; size: 2; recVer and recInstance 184 $verInstance = Xls::getUInt2d($this->data, $this->pos); 185 186 // offset: 2; size: 2: Record Type 187 $fbt = Xls::getUInt2d($this->data, $this->pos + 2); 188 189 // bit: 0-3; mask: 0x000F; recVer 190 $recVer = (0x000F & $verInstance) >> 0; 191 192 $length = Xls::getInt4d($this->data, $this->pos + 4); 193 $recordData = substr($this->data, $this->pos + 8, $length); 194 195 // move stream pointer to next record 196 $this->pos += 8 + $length; 197 } 198 199 /** 200 * Read DggContainer record (Drawing Group Container). 201 */ 202 private function readDggContainer() 203 { 204 $length = Xls::getInt4d($this->data, $this->pos + 4); 205 $recordData = substr($this->data, $this->pos + 8, $length); 206 207 // move stream pointer to next record 208 $this->pos += 8 + $length; 209 210 // record is a container, read contents 211 $dggContainer = new DggContainer(); 212 $this->object->setDggContainer($dggContainer); 213 $reader = new self($dggContainer); 214 $reader->load($recordData); 215 } 216 217 /** 218 * Read Dgg record (Drawing Group). 219 */ 220 private function readDgg() 221 { 222 $length = Xls::getInt4d($this->data, $this->pos + 4); 223 $recordData = substr($this->data, $this->pos + 8, $length); 224 225 // move stream pointer to next record 226 $this->pos += 8 + $length; 227 } 228 229 /** 230 * Read BstoreContainer record (Blip Store Container). 231 */ 232 private function readBstoreContainer() 233 { 234 $length = Xls::getInt4d($this->data, $this->pos + 4); 235 $recordData = substr($this->data, $this->pos + 8, $length); 236 237 // move stream pointer to next record 238 $this->pos += 8 + $length; 239 240 // record is a container, read contents 241 $bstoreContainer = new BstoreContainer(); 242 $this->object->setBstoreContainer($bstoreContainer); 243 $reader = new self($bstoreContainer); 244 $reader->load($recordData); 245 } 246 247 /** 248 * Read BSE record. 249 */ 250 private function readBSE() 251 { 252 // offset: 0; size: 2; recVer and recInstance 253 254 // bit: 4-15; mask: 0xFFF0; recInstance 255 $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; 256 257 $length = Xls::getInt4d($this->data, $this->pos + 4); 258 $recordData = substr($this->data, $this->pos + 8, $length); 259 260 // move stream pointer to next record 261 $this->pos += 8 + $length; 262 263 // add BSE to BstoreContainer 264 $BSE = new BSE(); 265 $this->object->addBSE($BSE); 266 267 $BSE->setBLIPType($recInstance); 268 269 // offset: 0; size: 1; btWin32 (MSOBLIPTYPE) 270 $btWin32 = ord($recordData[0]); 271 272 // offset: 1; size: 1; btWin32 (MSOBLIPTYPE) 273 $btMacOS = ord($recordData[1]); 274 275 // offset: 2; size: 16; MD4 digest 276 $rgbUid = substr($recordData, 2, 16); 277 278 // offset: 18; size: 2; tag 279 $tag = Xls::getUInt2d($recordData, 18); 280 281 // offset: 20; size: 4; size of BLIP in bytes 282 $size = Xls::getInt4d($recordData, 20); 283 284 // offset: 24; size: 4; number of references to this BLIP 285 $cRef = Xls::getInt4d($recordData, 24); 286 287 // offset: 28; size: 4; MSOFO file offset 288 $foDelay = Xls::getInt4d($recordData, 28); 289 290 // offset: 32; size: 1; unused1 291 $unused1 = ord($recordData[32]); 292 293 // offset: 33; size: 1; size of nameData in bytes (including null terminator) 294 $cbName = ord($recordData[33]); 295 296 // offset: 34; size: 1; unused2 297 $unused2 = ord($recordData[34]); 298 299 // offset: 35; size: 1; unused3 300 $unused3 = ord($recordData[35]); 301 302 // offset: 36; size: $cbName; nameData 303 $nameData = substr($recordData, 36, $cbName); 304 305 // offset: 36 + $cbName, size: var; the BLIP data 306 $blipData = substr($recordData, 36 + $cbName); 307 308 // record is a container, read contents 309 $reader = new self($BSE); 310 $reader->load($blipData); 311 } 312 313 /** 314 * Read BlipJPEG record. Holds raw JPEG image data. 315 */ 316 private function readBlipJPEG() 317 { 318 // offset: 0; size: 2; recVer and recInstance 319 320 // bit: 4-15; mask: 0xFFF0; recInstance 321 $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; 322 323 $length = Xls::getInt4d($this->data, $this->pos + 4); 324 $recordData = substr($this->data, $this->pos + 8, $length); 325 326 // move stream pointer to next record 327 $this->pos += 8 + $length; 328 329 $pos = 0; 330 331 // offset: 0; size: 16; rgbUid1 (MD4 digest of) 332 $rgbUid1 = substr($recordData, 0, 16); 333 $pos += 16; 334 335 // offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3 336 if (in_array($recInstance, [0x046B, 0x06E3])) { 337 $rgbUid2 = substr($recordData, 16, 16); 338 $pos += 16; 339 } 340 341 // offset: var; size: 1; tag 342 $tag = ord($recordData[$pos]); 343 $pos += 1; 344 345 // offset: var; size: var; the raw image data 346 $data = substr($recordData, $pos); 347 348 $blip = new Blip(); 349 $blip->setData($data); 350 351 $this->object->setBlip($blip); 352 } 353 354 /** 355 * Read BlipPNG record. Holds raw PNG image data. 356 */ 357 private function readBlipPNG() 358 { 359 // offset: 0; size: 2; recVer and recInstance 360 361 // bit: 4-15; mask: 0xFFF0; recInstance 362 $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; 363 364 $length = Xls::getInt4d($this->data, $this->pos + 4); 365 $recordData = substr($this->data, $this->pos + 8, $length); 366 367 // move stream pointer to next record 368 $this->pos += 8 + $length; 369 370 $pos = 0; 371 372 // offset: 0; size: 16; rgbUid1 (MD4 digest of) 373 $rgbUid1 = substr($recordData, 0, 16); 374 $pos += 16; 375 376 // offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3 377 if ($recInstance == 0x06E1) { 378 $rgbUid2 = substr($recordData, 16, 16); 379 $pos += 16; 380 } 381 382 // offset: var; size: 1; tag 383 $tag = ord($recordData[$pos]); 384 $pos += 1; 385 386 // offset: var; size: var; the raw image data 387 $data = substr($recordData, $pos); 388 389 $blip = new Blip(); 390 $blip->setData($data); 391 392 $this->object->setBlip($blip); 393 } 394 395 /** 396 * Read OPT record. This record may occur within DggContainer record or SpContainer. 397 */ 398 private function readOPT() 399 { 400 // offset: 0; size: 2; recVer and recInstance 401 402 // bit: 4-15; mask: 0xFFF0; recInstance 403 $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; 404 405 $length = Xls::getInt4d($this->data, $this->pos + 4); 406 $recordData = substr($this->data, $this->pos + 8, $length); 407 408 // move stream pointer to next record 409 $this->pos += 8 + $length; 410 411 $this->readOfficeArtRGFOPTE($recordData, $recInstance); 412 } 413 414 /** 415 * Read TertiaryOPT record. 416 */ 417 private function readTertiaryOPT() 418 { 419 // offset: 0; size: 2; recVer and recInstance 420 421 // bit: 4-15; mask: 0xFFF0; recInstance 422 $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; 423 424 $length = Xls::getInt4d($this->data, $this->pos + 4); 425 $recordData = substr($this->data, $this->pos + 8, $length); 426 427 // move stream pointer to next record 428 $this->pos += 8 + $length; 429 } 430 431 /** 432 * Read SplitMenuColors record. 433 */ 434 private function readSplitMenuColors() 435 { 436 $length = Xls::getInt4d($this->data, $this->pos + 4); 437 $recordData = substr($this->data, $this->pos + 8, $length); 438 439 // move stream pointer to next record 440 $this->pos += 8 + $length; 441 } 442 443 /** 444 * Read DgContainer record (Drawing Container). 445 */ 446 private function readDgContainer() 447 { 448 $length = Xls::getInt4d($this->data, $this->pos + 4); 449 $recordData = substr($this->data, $this->pos + 8, $length); 450 451 // move stream pointer to next record 452 $this->pos += 8 + $length; 453 454 // record is a container, read contents 455 $dgContainer = new DgContainer(); 456 $this->object->setDgContainer($dgContainer); 457 $reader = new self($dgContainer); 458 $escher = $reader->load($recordData); 459 } 460 461 /** 462 * Read Dg record (Drawing). 463 */ 464 private function readDg() 465 { 466 $length = Xls::getInt4d($this->data, $this->pos + 4); 467 $recordData = substr($this->data, $this->pos + 8, $length); 468 469 // move stream pointer to next record 470 $this->pos += 8 + $length; 471 } 472 473 /** 474 * Read SpgrContainer record (Shape Group Container). 475 */ 476 private function readSpgrContainer() 477 { 478 // context is either context DgContainer or SpgrContainer 479 480 $length = Xls::getInt4d($this->data, $this->pos + 4); 481 $recordData = substr($this->data, $this->pos + 8, $length); 482 483 // move stream pointer to next record 484 $this->pos += 8 + $length; 485 486 // record is a container, read contents 487 $spgrContainer = new SpgrContainer(); 488 489 if ($this->object instanceof DgContainer) { 490 // DgContainer 491 $this->object->setSpgrContainer($spgrContainer); 492 } else { 493 // SpgrContainer 494 $this->object->addChild($spgrContainer); 495 } 496 497 $reader = new self($spgrContainer); 498 $escher = $reader->load($recordData); 499 } 500 501 /** 502 * Read SpContainer record (Shape Container). 503 */ 504 private function readSpContainer() 505 { 506 $length = Xls::getInt4d($this->data, $this->pos + 4); 507 $recordData = substr($this->data, $this->pos + 8, $length); 508 509 // add spContainer to spgrContainer 510 $spContainer = new SpContainer(); 511 $this->object->addChild($spContainer); 512 513 // move stream pointer to next record 514 $this->pos += 8 + $length; 515 516 // record is a container, read contents 517 $reader = new self($spContainer); 518 $escher = $reader->load($recordData); 519 } 520 521 /** 522 * Read Spgr record (Shape Group). 523 */ 524 private function readSpgr() 525 { 526 $length = Xls::getInt4d($this->data, $this->pos + 4); 527 $recordData = substr($this->data, $this->pos + 8, $length); 528 529 // move stream pointer to next record 530 $this->pos += 8 + $length; 531 } 532 533 /** 534 * Read Sp record (Shape). 535 */ 536 private function readSp() 537 { 538 // offset: 0; size: 2; recVer and recInstance 539 540 // bit: 4-15; mask: 0xFFF0; recInstance 541 $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; 542 543 $length = Xls::getInt4d($this->data, $this->pos + 4); 544 $recordData = substr($this->data, $this->pos + 8, $length); 545 546 // move stream pointer to next record 547 $this->pos += 8 + $length; 548 } 549 550 /** 551 * Read ClientTextbox record. 552 */ 553 private function readClientTextbox() 554 { 555 // offset: 0; size: 2; recVer and recInstance 556 557 // bit: 4-15; mask: 0xFFF0; recInstance 558 $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; 559 560 $length = Xls::getInt4d($this->data, $this->pos + 4); 561 $recordData = substr($this->data, $this->pos + 8, $length); 562 563 // move stream pointer to next record 564 $this->pos += 8 + $length; 565 } 566 567 /** 568 * Read ClientAnchor record. This record holds information about where the shape is anchored in worksheet. 569 */ 570 private function readClientAnchor() 571 { 572 $length = Xls::getInt4d($this->data, $this->pos + 4); 573 $recordData = substr($this->data, $this->pos + 8, $length); 574 575 // move stream pointer to next record 576 $this->pos += 8 + $length; 577 578 // offset: 2; size: 2; upper-left corner column index (0-based) 579 $c1 = Xls::getUInt2d($recordData, 2); 580 581 // offset: 4; size: 2; upper-left corner horizontal offset in 1/1024 of column width 582 $startOffsetX = Xls::getUInt2d($recordData, 4); 583 584 // offset: 6; size: 2; upper-left corner row index (0-based) 585 $r1 = Xls::getUInt2d($recordData, 6); 586 587 // offset: 8; size: 2; upper-left corner vertical offset in 1/256 of row height 588 $startOffsetY = Xls::getUInt2d($recordData, 8); 589 590 // offset: 10; size: 2; bottom-right corner column index (0-based) 591 $c2 = Xls::getUInt2d($recordData, 10); 592 593 // offset: 12; size: 2; bottom-right corner horizontal offset in 1/1024 of column width 594 $endOffsetX = Xls::getUInt2d($recordData, 12); 595 596 // offset: 14; size: 2; bottom-right corner row index (0-based) 597 $r2 = Xls::getUInt2d($recordData, 14); 598 599 // offset: 16; size: 2; bottom-right corner vertical offset in 1/256 of row height 600 $endOffsetY = Xls::getUInt2d($recordData, 16); 601 602 // set the start coordinates 603 $this->object->setStartCoordinates(Coordinate::stringFromColumnIndex($c1 + 1) . ($r1 + 1)); 604 605 // set the start offsetX 606 $this->object->setStartOffsetX($startOffsetX); 607 608 // set the start offsetY 609 $this->object->setStartOffsetY($startOffsetY); 610 611 // set the end coordinates 612 $this->object->setEndCoordinates(Coordinate::stringFromColumnIndex($c2 + 1) . ($r2 + 1)); 613 614 // set the end offsetX 615 $this->object->setEndOffsetX($endOffsetX); 616 617 // set the end offsetY 618 $this->object->setEndOffsetY($endOffsetY); 619 } 620 621 /** 622 * Read ClientData record. 623 */ 624 private function readClientData() 625 { 626 $length = Xls::getInt4d($this->data, $this->pos + 4); 627 $recordData = substr($this->data, $this->pos + 8, $length); 628 629 // move stream pointer to next record 630 $this->pos += 8 + $length; 631 } 632 633 /** 634 * Read OfficeArtRGFOPTE table of property-value pairs. 635 * 636 * @param string $data Binary data 637 * @param int $n Number of properties 638 */ 639 private function readOfficeArtRGFOPTE($data, $n) 640 { 641 $splicedComplexData = substr($data, 6 * $n); 642 643 // loop through property-value pairs 644 for ($i = 0; $i < $n; ++$i) { 645 // read 6 bytes at a time 646 $fopte = substr($data, 6 * $i, 6); 647 648 // offset: 0; size: 2; opid 649 $opid = Xls::getUInt2d($fopte, 0); 650 651 // bit: 0-13; mask: 0x3FFF; opid.opid 652 $opidOpid = (0x3FFF & $opid) >> 0; 653 654 // bit: 14; mask 0x4000; 1 = value in op field is BLIP identifier 655 $opidFBid = (0x4000 & $opid) >> 14; 656 657 // bit: 15; mask 0x8000; 1 = this is a complex property, op field specifies size of complex data 658 $opidFComplex = (0x8000 & $opid) >> 15; 659 660 // offset: 2; size: 4; the value for this property 661 $op = Xls::getInt4d($fopte, 2); 662 663 if ($opidFComplex) { 664 $complexData = substr($splicedComplexData, 0, $op); 665 $splicedComplexData = substr($splicedComplexData, $op); 666 667 // we store string value with complex data 668 $value = $complexData; 669 } else { 670 // we store integer value 671 $value = $op; 672 } 673 674 $this->object->setOPT($opidOpid, $value); 675 } 676 } 677} 678