1<?php 2 3namespace PhpOffice\PhpSpreadsheet\Writer\Xls; 4 5use PhpOffice\PhpSpreadsheet\Cell\Coordinate; 6use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer; 7use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer; 8use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer; 9use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer; 10use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer; 11use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE; 12use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE\Blip; 13 14class Escher 15{ 16 /** 17 * The object we are writing. 18 */ 19 private $object; 20 21 /** 22 * The written binary data. 23 */ 24 private $data; 25 26 /** 27 * Shape offsets. Positions in binary stream where a new shape record begins. 28 * 29 * @var array 30 */ 31 private $spOffsets; 32 33 /** 34 * Shape types. 35 * 36 * @var array 37 */ 38 private $spTypes; 39 40 /** 41 * Constructor. 42 * 43 * @param mixed $object 44 */ 45 public function __construct($object) 46 { 47 $this->object = $object; 48 } 49 50 /** 51 * Process the object to be written. 52 * 53 * @return string 54 */ 55 public function close() 56 { 57 // initialize 58 $this->data = ''; 59 60 switch (get_class($this->object)) { 61 case \PhpOffice\PhpSpreadsheet\Shared\Escher::class: 62 if ($dggContainer = $this->object->getDggContainer()) { 63 $writer = new self($dggContainer); 64 $this->data = $writer->close(); 65 } elseif ($dgContainer = $this->object->getDgContainer()) { 66 $writer = new self($dgContainer); 67 $this->data = $writer->close(); 68 $this->spOffsets = $writer->getSpOffsets(); 69 $this->spTypes = $writer->getSpTypes(); 70 } 71 72 break; 73 case DggContainer::class: 74 // this is a container record 75 76 // initialize 77 $innerData = ''; 78 79 // write the dgg 80 $recVer = 0x0; 81 $recInstance = 0x0000; 82 $recType = 0xF006; 83 84 $recVerInstance = $recVer; 85 $recVerInstance |= $recInstance << 4; 86 87 // dgg data 88 $dggData = 89 pack( 90 'VVVV', 91 $this->object->getSpIdMax(), // maximum shape identifier increased by one 92 $this->object->getCDgSaved() + 1, // number of file identifier clusters increased by one 93 $this->object->getCSpSaved(), 94 $this->object->getCDgSaved() // count total number of drawings saved 95 ); 96 97 // add file identifier clusters (one per drawing) 98 $IDCLs = $this->object->getIDCLs(); 99 100 foreach ($IDCLs as $dgId => $maxReducedSpId) { 101 $dggData .= pack('VV', $dgId, $maxReducedSpId + 1); 102 } 103 104 $header = pack('vvV', $recVerInstance, $recType, strlen($dggData)); 105 $innerData .= $header . $dggData; 106 107 // write the bstoreContainer 108 if ($bstoreContainer = $this->object->getBstoreContainer()) { 109 $writer = new self($bstoreContainer); 110 $innerData .= $writer->close(); 111 } 112 113 // write the record 114 $recVer = 0xF; 115 $recInstance = 0x0000; 116 $recType = 0xF000; 117 $length = strlen($innerData); 118 119 $recVerInstance = $recVer; 120 $recVerInstance |= $recInstance << 4; 121 122 $header = pack('vvV', $recVerInstance, $recType, $length); 123 124 $this->data = $header . $innerData; 125 126 break; 127 case BstoreContainer::class: 128 // this is a container record 129 130 // initialize 131 $innerData = ''; 132 133 // treat the inner data 134 if ($BSECollection = $this->object->getBSECollection()) { 135 foreach ($BSECollection as $BSE) { 136 $writer = new self($BSE); 137 $innerData .= $writer->close(); 138 } 139 } 140 141 // write the record 142 $recVer = 0xF; 143 $recInstance = count($this->object->getBSECollection()); 144 $recType = 0xF001; 145 $length = strlen($innerData); 146 147 $recVerInstance = $recVer; 148 $recVerInstance |= $recInstance << 4; 149 150 $header = pack('vvV', $recVerInstance, $recType, $length); 151 152 $this->data = $header . $innerData; 153 154 break; 155 case BSE::class: 156 // this is a semi-container record 157 158 // initialize 159 $innerData = ''; 160 161 // here we treat the inner data 162 if ($blip = $this->object->getBlip()) { 163 $writer = new self($blip); 164 $innerData .= $writer->close(); 165 } 166 167 // initialize 168 $data = ''; 169 170 $btWin32 = $this->object->getBlipType(); 171 $btMacOS = $this->object->getBlipType(); 172 $data .= pack('CC', $btWin32, $btMacOS); 173 174 $rgbUid = pack('VVVV', 0, 0, 0, 0); // todo 175 $data .= $rgbUid; 176 177 $tag = 0; 178 $size = strlen($innerData); 179 $cRef = 1; 180 $foDelay = 0; //todo 181 $unused1 = 0x0; 182 $cbName = 0x0; 183 $unused2 = 0x0; 184 $unused3 = 0x0; 185 $data .= pack('vVVVCCCC', $tag, $size, $cRef, $foDelay, $unused1, $cbName, $unused2, $unused3); 186 187 $data .= $innerData; 188 189 // write the record 190 $recVer = 0x2; 191 $recInstance = $this->object->getBlipType(); 192 $recType = 0xF007; 193 $length = strlen($data); 194 195 $recVerInstance = $recVer; 196 $recVerInstance |= $recInstance << 4; 197 198 $header = pack('vvV', $recVerInstance, $recType, $length); 199 200 $this->data = $header; 201 202 $this->data .= $data; 203 204 break; 205 case Blip::class: 206 // this is an atom record 207 208 // write the record 209 switch ($this->object->getParent()->getBlipType()) { 210 case BSE::BLIPTYPE_JPEG: 211 // initialize 212 $innerData = ''; 213 214 $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo 215 $innerData .= $rgbUid1; 216 217 $tag = 0xFF; // todo 218 $innerData .= pack('C', $tag); 219 220 $innerData .= $this->object->getData(); 221 222 $recVer = 0x0; 223 $recInstance = 0x46A; 224 $recType = 0xF01D; 225 $length = strlen($innerData); 226 227 $recVerInstance = $recVer; 228 $recVerInstance |= $recInstance << 4; 229 230 $header = pack('vvV', $recVerInstance, $recType, $length); 231 232 $this->data = $header; 233 234 $this->data .= $innerData; 235 236 break; 237 case BSE::BLIPTYPE_PNG: 238 // initialize 239 $innerData = ''; 240 241 $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo 242 $innerData .= $rgbUid1; 243 244 $tag = 0xFF; // todo 245 $innerData .= pack('C', $tag); 246 247 $innerData .= $this->object->getData(); 248 249 $recVer = 0x0; 250 $recInstance = 0x6E0; 251 $recType = 0xF01E; 252 $length = strlen($innerData); 253 254 $recVerInstance = $recVer; 255 $recVerInstance |= $recInstance << 4; 256 257 $header = pack('vvV', $recVerInstance, $recType, $length); 258 259 $this->data = $header; 260 261 $this->data .= $innerData; 262 263 break; 264 } 265 266 break; 267 case DgContainer::class: 268 // this is a container record 269 270 // initialize 271 $innerData = ''; 272 273 // write the dg 274 $recVer = 0x0; 275 $recInstance = $this->object->getDgId(); 276 $recType = 0xF008; 277 $length = 8; 278 279 $recVerInstance = $recVer; 280 $recVerInstance |= $recInstance << 4; 281 282 $header = pack('vvV', $recVerInstance, $recType, $length); 283 284 // number of shapes in this drawing (including group shape) 285 $countShapes = count($this->object->getSpgrContainer()->getChildren()); 286 $innerData .= $header . pack('VV', $countShapes, $this->object->getLastSpId()); 287 288 // write the spgrContainer 289 if ($spgrContainer = $this->object->getSpgrContainer()) { 290 $writer = new self($spgrContainer); 291 $innerData .= $writer->close(); 292 293 // get the shape offsets relative to the spgrContainer record 294 $spOffsets = $writer->getSpOffsets(); 295 $spTypes = $writer->getSpTypes(); 296 297 // save the shape offsets relative to dgContainer 298 foreach ($spOffsets as &$spOffset) { 299 $spOffset += 24; // add length of dgContainer header data (8 bytes) plus dg data (16 bytes) 300 } 301 302 $this->spOffsets = $spOffsets; 303 $this->spTypes = $spTypes; 304 } 305 306 // write the record 307 $recVer = 0xF; 308 $recInstance = 0x0000; 309 $recType = 0xF002; 310 $length = strlen($innerData); 311 312 $recVerInstance = $recVer; 313 $recVerInstance |= $recInstance << 4; 314 315 $header = pack('vvV', $recVerInstance, $recType, $length); 316 317 $this->data = $header . $innerData; 318 319 break; 320 case SpgrContainer::class: 321 // this is a container record 322 323 // initialize 324 $innerData = ''; 325 326 // initialize spape offsets 327 $totalSize = 8; 328 $spOffsets = []; 329 $spTypes = []; 330 331 // treat the inner data 332 foreach ($this->object->getChildren() as $spContainer) { 333 $writer = new self($spContainer); 334 $spData = $writer->close(); 335 $innerData .= $spData; 336 337 // save the shape offsets (where new shape records begin) 338 $totalSize += strlen($spData); 339 $spOffsets[] = $totalSize; 340 341 $spTypes = array_merge($spTypes, $writer->getSpTypes()); 342 } 343 344 // write the record 345 $recVer = 0xF; 346 $recInstance = 0x0000; 347 $recType = 0xF003; 348 $length = strlen($innerData); 349 350 $recVerInstance = $recVer; 351 $recVerInstance |= $recInstance << 4; 352 353 $header = pack('vvV', $recVerInstance, $recType, $length); 354 355 $this->data = $header . $innerData; 356 $this->spOffsets = $spOffsets; 357 $this->spTypes = $spTypes; 358 359 break; 360 case SpContainer::class: 361 // initialize 362 $data = ''; 363 364 // build the data 365 366 // write group shape record, if necessary? 367 if ($this->object->getSpgr()) { 368 $recVer = 0x1; 369 $recInstance = 0x0000; 370 $recType = 0xF009; 371 $length = 0x00000010; 372 373 $recVerInstance = $recVer; 374 $recVerInstance |= $recInstance << 4; 375 376 $header = pack('vvV', $recVerInstance, $recType, $length); 377 378 $data .= $header . pack('VVVV', 0, 0, 0, 0); 379 } 380 $this->spTypes[] = ($this->object->getSpType()); 381 382 // write the shape record 383 $recVer = 0x2; 384 $recInstance = $this->object->getSpType(); // shape type 385 $recType = 0xF00A; 386 $length = 0x00000008; 387 388 $recVerInstance = $recVer; 389 $recVerInstance |= $recInstance << 4; 390 391 $header = pack('vvV', $recVerInstance, $recType, $length); 392 393 $data .= $header . pack('VV', $this->object->getSpId(), $this->object->getSpgr() ? 0x0005 : 0x0A00); 394 395 // the options 396 if ($this->object->getOPTCollection()) { 397 $optData = ''; 398 399 $recVer = 0x3; 400 $recInstance = count($this->object->getOPTCollection()); 401 $recType = 0xF00B; 402 foreach ($this->object->getOPTCollection() as $property => $value) { 403 $optData .= pack('vV', $property, $value); 404 } 405 $length = strlen($optData); 406 407 $recVerInstance = $recVer; 408 $recVerInstance |= $recInstance << 4; 409 410 $header = pack('vvV', $recVerInstance, $recType, $length); 411 $data .= $header . $optData; 412 } 413 414 // the client anchor 415 if ($this->object->getStartCoordinates()) { 416 $clientAnchorData = ''; 417 418 $recVer = 0x0; 419 $recInstance = 0x0; 420 $recType = 0xF010; 421 422 // start coordinates 423 [$column, $row] = Coordinate::indexesFromString($this->object->getStartCoordinates()); 424 $c1 = $column - 1; 425 $r1 = $row - 1; 426 427 // start offsetX 428 $startOffsetX = $this->object->getStartOffsetX(); 429 430 // start offsetY 431 $startOffsetY = $this->object->getStartOffsetY(); 432 433 // end coordinates 434 [$column, $row] = Coordinate::indexesFromString($this->object->getEndCoordinates()); 435 $c2 = $column - 1; 436 $r2 = $row - 1; 437 438 // end offsetX 439 $endOffsetX = $this->object->getEndOffsetX(); 440 441 // end offsetY 442 $endOffsetY = $this->object->getEndOffsetY(); 443 444 $clientAnchorData = pack('vvvvvvvvv', $this->object->getSpFlag(), $c1, $startOffsetX, $r1, $startOffsetY, $c2, $endOffsetX, $r2, $endOffsetY); 445 446 $length = strlen($clientAnchorData); 447 448 $recVerInstance = $recVer; 449 $recVerInstance |= $recInstance << 4; 450 451 $header = pack('vvV', $recVerInstance, $recType, $length); 452 $data .= $header . $clientAnchorData; 453 } 454 455 // the client data, just empty for now 456 if (!$this->object->getSpgr()) { 457 $clientDataData = ''; 458 459 $recVer = 0x0; 460 $recInstance = 0x0; 461 $recType = 0xF011; 462 463 $length = strlen($clientDataData); 464 465 $recVerInstance = $recVer; 466 $recVerInstance |= $recInstance << 4; 467 468 $header = pack('vvV', $recVerInstance, $recType, $length); 469 $data .= $header . $clientDataData; 470 } 471 472 // write the record 473 $recVer = 0xF; 474 $recInstance = 0x0000; 475 $recType = 0xF004; 476 $length = strlen($data); 477 478 $recVerInstance = $recVer; 479 $recVerInstance |= $recInstance << 4; 480 481 $header = pack('vvV', $recVerInstance, $recType, $length); 482 483 $this->data = $header . $data; 484 485 break; 486 } 487 488 return $this->data; 489 } 490 491 /** 492 * Gets the shape offsets. 493 * 494 * @return array 495 */ 496 public function getSpOffsets() 497 { 498 return $this->spOffsets; 499 } 500 501 /** 502 * Gets the shape types. 503 * 504 * @return array 505 */ 506 public function getSpTypes() 507 { 508 return $this->spTypes; 509 } 510} 511