Escher.php 20 KB

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