8889841cPK[roggColor/BIFF5.phpnu[ '000000', 0x09 => 'FFFFFF', 0x0A => 'FF0000', 0x0B => '00FF00', 0x0C => '0000FF', 0x0D => 'FFFF00', 0x0E => 'FF00FF', 0x0F => '00FFFF', 0x10 => '800000', 0x11 => '008000', 0x12 => '000080', 0x13 => '808000', 0x14 => '800080', 0x15 => '008080', 0x16 => 'C0C0C0', 0x17 => '808080', 0x18 => '8080FF', 0x19 => '802060', 0x1A => 'FFFFC0', 0x1B => 'A0E0F0', 0x1C => '600080', 0x1D => 'FF8080', 0x1E => '0080C0', 0x1F => 'C0C0FF', 0x20 => '000080', 0x21 => 'FF00FF', 0x22 => 'FFFF00', 0x23 => '00FFFF', 0x24 => '800080', 0x25 => '800000', 0x26 => '008080', 0x27 => '0000FF', 0x28 => '00CFFF', 0x29 => '69FFFF', 0x2A => 'E0FFE0', 0x2B => 'FFFF80', 0x2C => 'A6CAF0', 0x2D => 'DD9CB3', 0x2E => 'B38FEE', 0x2F => 'E3E3E3', 0x30 => '2A6FF9', 0x31 => '3FB8CD', 0x32 => '488436', 0x33 => '958C41', 0x34 => '8E5E42', 0x35 => 'A0627A', 0x36 => '624FAC', 0x37 => '969696', 0x38 => '1D2FBE', 0x39 => '286676', 0x3A => '004500', 0x3B => '453E01', 0x3C => '6A2813', 0x3D => '85396A', 0x3E => '4A3285', 0x3F => '424242', ]; /** * Map color array from BIFF5 built-in color index. * * @param int $color * * @return array */ public static function lookup($color) { if (isset(self::$map[$color])) { return ['rgb' => self::$map[$color]]; } return ['rgb' => '000000']; } } PK[#팯Color/BuiltIn.phpnu[ '000000', 0x01 => 'FFFFFF', 0x02 => 'FF0000', 0x03 => '00FF00', 0x04 => '0000FF', 0x05 => 'FFFF00', 0x06 => 'FF00FF', 0x07 => '00FFFF', 0x40 => '000000', // system window text color 0x41 => 'FFFFFF', // system window background color ]; /** * Map built-in color to RGB value. * * @param int $color Indexed color * * @return array */ public static function lookup($color) { if (isset(self::$map[$color])) { return ['rgb' => self::$map[$color]]; } return ['rgb' => '000000']; } } PK[$pggColor/BIFF8.phpnu[ '000000', 0x09 => 'FFFFFF', 0x0A => 'FF0000', 0x0B => '00FF00', 0x0C => '0000FF', 0x0D => 'FFFF00', 0x0E => 'FF00FF', 0x0F => '00FFFF', 0x10 => '800000', 0x11 => '008000', 0x12 => '000080', 0x13 => '808000', 0x14 => '800080', 0x15 => '008080', 0x16 => 'C0C0C0', 0x17 => '808080', 0x18 => '9999FF', 0x19 => '993366', 0x1A => 'FFFFCC', 0x1B => 'CCFFFF', 0x1C => '660066', 0x1D => 'FF8080', 0x1E => '0066CC', 0x1F => 'CCCCFF', 0x20 => '000080', 0x21 => 'FF00FF', 0x22 => 'FFFF00', 0x23 => '00FFFF', 0x24 => '800080', 0x25 => '800000', 0x26 => '008080', 0x27 => '0000FF', 0x28 => '00CCFF', 0x29 => 'CCFFFF', 0x2A => 'CCFFCC', 0x2B => 'FFFF99', 0x2C => '99CCFF', 0x2D => 'FF99CC', 0x2E => 'CC99FF', 0x2F => 'FFCC99', 0x30 => '3366FF', 0x31 => '33CCCC', 0x32 => '99CC00', 0x33 => 'FFCC00', 0x34 => 'FF9900', 0x35 => 'FF6600', 0x36 => '666699', 0x37 => '969696', 0x38 => '003366', 0x39 => '339966', 0x3A => '003300', 0x3B => '333300', 0x3C => '993300', 0x3D => '993366', 0x3E => '333399', 0x3F => '333333', ]; /** * Map color array from BIFF8 built-in color index. * * @param int $color * * @return array */ public static function lookup($color) { if (isset(self::$map[$color])) { return ['rgb' => self::$map[$color]]; } return ['rgb' => '000000']; } } PK[Hl Color.phpnu[ 'FF0000'] */ public static function map($color, $palette, $version) { if ($color <= 0x07 || $color >= 0x40) { // special built-in color return Color\BuiltIn::lookup($color); } elseif (isset($palette[$color - 8])) { // palette color, color index 0x08 maps to pallete index 0 return $palette[$color - 8]; } // default color table if ($version == Xls::XLS_BIFF8) { return Color\BIFF8::lookup($color); } // BIFF5 return Color\BIFF5::lookup($color); } } PK[RC4.phpnu[i = 0; $this->i < 256; ++$this->i) { $this->s[$this->i] = $this->i; } $this->j = 0; for ($this->i = 0; $this->i < 256; ++$this->i) { $this->j = ($this->j + $this->s[$this->i] + ord($key[$this->i % $len])) % 256; $t = $this->s[$this->i]; $this->s[$this->i] = $this->s[$this->j]; $this->s[$this->j] = $t; } $this->i = $this->j = 0; } /** * Symmetric decryption/encryption function. * * @param string $data Data to encrypt/decrypt * * @return string */ public function RC4($data) { $len = strlen($data); for ($c = 0; $c < $len; ++$c) { $this->i = ($this->i + 1) % 256; $this->j = ($this->j + $this->s[$this->i]) % 256; $t = $this->s[$this->i]; $this->s[$this->i] = $this->s[$this->j]; $this->s[$this->j] = $t; $t = ($this->s[$this->i] + $this->s[$this->j]) % 256; $data[$c] = chr(ord($data[$c]) ^ $this->s[$t]); } return $data; } } PK[ :$MMMD5.phpnu[reset(); } /** * Reset the MD5 stream context. */ public function reset(): void { $this->a = 0x67452301; $this->b = 0xEFCDAB89; $this->c = 0x98BADCFE; $this->d = 0x10325476; } /** * Get MD5 stream context. * * @return string */ public function getContext() { $s = ''; foreach (['a', 'b', 'c', 'd'] as $i) { $v = $this->{$i}; $s .= chr($v & 0xff); $s .= chr(($v >> 8) & 0xff); $s .= chr(($v >> 16) & 0xff); $s .= chr(($v >> 24) & 0xff); } return $s; } /** * Add data to context. * * @param string $data Data to add */ public function add(string $data): void { $words = array_values(unpack('V16', $data)); $A = $this->a; $B = $this->b; $C = $this->c; $D = $this->d; $F = ['self', 'f']; $G = ['self', 'g']; $H = ['self', 'h']; $I = ['self', 'i']; // ROUND 1 self::step($F, $A, $B, $C, $D, $words[0], 7, 0xd76aa478); self::step($F, $D, $A, $B, $C, $words[1], 12, 0xe8c7b756); self::step($F, $C, $D, $A, $B, $words[2], 17, 0x242070db); self::step($F, $B, $C, $D, $A, $words[3], 22, 0xc1bdceee); self::step($F, $A, $B, $C, $D, $words[4], 7, 0xf57c0faf); self::step($F, $D, $A, $B, $C, $words[5], 12, 0x4787c62a); self::step($F, $C, $D, $A, $B, $words[6], 17, 0xa8304613); self::step($F, $B, $C, $D, $A, $words[7], 22, 0xfd469501); self::step($F, $A, $B, $C, $D, $words[8], 7, 0x698098d8); self::step($F, $D, $A, $B, $C, $words[9], 12, 0x8b44f7af); self::step($F, $C, $D, $A, $B, $words[10], 17, 0xffff5bb1); self::step($F, $B, $C, $D, $A, $words[11], 22, 0x895cd7be); self::step($F, $A, $B, $C, $D, $words[12], 7, 0x6b901122); self::step($F, $D, $A, $B, $C, $words[13], 12, 0xfd987193); self::step($F, $C, $D, $A, $B, $words[14], 17, 0xa679438e); self::step($F, $B, $C, $D, $A, $words[15], 22, 0x49b40821); // ROUND 2 self::step($G, $A, $B, $C, $D, $words[1], 5, 0xf61e2562); self::step($G, $D, $A, $B, $C, $words[6], 9, 0xc040b340); self::step($G, $C, $D, $A, $B, $words[11], 14, 0x265e5a51); self::step($G, $B, $C, $D, $A, $words[0], 20, 0xe9b6c7aa); self::step($G, $A, $B, $C, $D, $words[5], 5, 0xd62f105d); self::step($G, $D, $A, $B, $C, $words[10], 9, 0x02441453); self::step($G, $C, $D, $A, $B, $words[15], 14, 0xd8a1e681); self::step($G, $B, $C, $D, $A, $words[4], 20, 0xe7d3fbc8); self::step($G, $A, $B, $C, $D, $words[9], 5, 0x21e1cde6); self::step($G, $D, $A, $B, $C, $words[14], 9, 0xc33707d6); self::step($G, $C, $D, $A, $B, $words[3], 14, 0xf4d50d87); self::step($G, $B, $C, $D, $A, $words[8], 20, 0x455a14ed); self::step($G, $A, $B, $C, $D, $words[13], 5, 0xa9e3e905); self::step($G, $D, $A, $B, $C, $words[2], 9, 0xfcefa3f8); self::step($G, $C, $D, $A, $B, $words[7], 14, 0x676f02d9); self::step($G, $B, $C, $D, $A, $words[12], 20, 0x8d2a4c8a); // ROUND 3 self::step($H, $A, $B, $C, $D, $words[5], 4, 0xfffa3942); self::step($H, $D, $A, $B, $C, $words[8], 11, 0x8771f681); self::step($H, $C, $D, $A, $B, $words[11], 16, 0x6d9d6122); self::step($H, $B, $C, $D, $A, $words[14], 23, 0xfde5380c); self::step($H, $A, $B, $C, $D, $words[1], 4, 0xa4beea44); self::step($H, $D, $A, $B, $C, $words[4], 11, 0x4bdecfa9); self::step($H, $C, $D, $A, $B, $words[7], 16, 0xf6bb4b60); self::step($H, $B, $C, $D, $A, $words[10], 23, 0xbebfbc70); self::step($H, $A, $B, $C, $D, $words[13], 4, 0x289b7ec6); self::step($H, $D, $A, $B, $C, $words[0], 11, 0xeaa127fa); self::step($H, $C, $D, $A, $B, $words[3], 16, 0xd4ef3085); self::step($H, $B, $C, $D, $A, $words[6], 23, 0x04881d05); self::step($H, $A, $B, $C, $D, $words[9], 4, 0xd9d4d039); self::step($H, $D, $A, $B, $C, $words[12], 11, 0xe6db99e5); self::step($H, $C, $D, $A, $B, $words[15], 16, 0x1fa27cf8); self::step($H, $B, $C, $D, $A, $words[2], 23, 0xc4ac5665); // ROUND 4 self::step($I, $A, $B, $C, $D, $words[0], 6, 0xf4292244); self::step($I, $D, $A, $B, $C, $words[7], 10, 0x432aff97); self::step($I, $C, $D, $A, $B, $words[14], 15, 0xab9423a7); self::step($I, $B, $C, $D, $A, $words[5], 21, 0xfc93a039); self::step($I, $A, $B, $C, $D, $words[12], 6, 0x655b59c3); self::step($I, $D, $A, $B, $C, $words[3], 10, 0x8f0ccc92); self::step($I, $C, $D, $A, $B, $words[10], 15, 0xffeff47d); self::step($I, $B, $C, $D, $A, $words[1], 21, 0x85845dd1); self::step($I, $A, $B, $C, $D, $words[8], 6, 0x6fa87e4f); self::step($I, $D, $A, $B, $C, $words[15], 10, 0xfe2ce6e0); self::step($I, $C, $D, $A, $B, $words[6], 15, 0xa3014314); self::step($I, $B, $C, $D, $A, $words[13], 21, 0x4e0811a1); self::step($I, $A, $B, $C, $D, $words[4], 6, 0xf7537e82); self::step($I, $D, $A, $B, $C, $words[11], 10, 0xbd3af235); self::step($I, $C, $D, $A, $B, $words[2], 15, 0x2ad7d2bb); self::step($I, $B, $C, $D, $A, $words[9], 21, 0xeb86d391); $this->a = ($this->a + $A) & 0xffffffff; $this->b = ($this->b + $B) & 0xffffffff; $this->c = ($this->c + $C) & 0xffffffff; $this->d = ($this->d + $D) & 0xffffffff; } private static function f(int $X, int $Y, int $Z) { return ($X & $Y) | ((~$X) & $Z); // X AND Y OR NOT X AND Z } private static function g(int $X, int $Y, int $Z) { return ($X & $Z) | ($Y & (~$Z)); // X AND Z OR Y AND NOT Z } private static function h(int $X, int $Y, int $Z) { return $X ^ $Y ^ $Z; // X XOR Y XOR Z } private static function i(int $X, int $Y, int $Z) { return $Y ^ ($X | (~$Z)); // Y XOR (X OR NOT Z) } private static function step($func, int &$A, int $B, int $C, int $D, int $M, int $s, int $t): void { $A = ($A + call_user_func($func, $B, $C, $D) + $M + $t) & 0xffffffff; $A = self::rotate($A, $s); $A = ($B + $A) & 0xffffffff; } private static function rotate(int $decimal, int $bits) { $binary = str_pad(decbin($decimal), 32, '0', STR_PAD_LEFT); return bindec(substr($binary, $bits) . substr($binary, 0, $bits)); } } PK[i44 ErrorCode.phpnu[ */ protected static $errorCodeMap = [ '#NULL!' => 0x00, '#DIV/0!' => 0x07, '#VALUE!' => 0x0F, '#REF!' => 0x17, '#NAME?' => 0x1D, '#NUM!' => 0x24, '#N/A' => 0x2A, ]; public static function error(string $errorCode): int { if (array_key_exists($errorCode, self::$errorCodeMap)) { return self::$errorCodeMap[$errorCode]; } return 0; } } PK[{AA Escher.phpnu[object = $object; } /** * Process the object to be written. * * @return string */ public function close() { // initialize $this->data = ''; switch (get_class($this->object)) { case \PhpOffice\PhpSpreadsheet\Shared\Escher::class: if ($dggContainer = $this->object->getDggContainer()) { $writer = new self($dggContainer); $this->data = $writer->close(); } elseif ($dgContainer = $this->object->getDgContainer()) { $writer = new self($dgContainer); $this->data = $writer->close(); $this->spOffsets = $writer->getSpOffsets(); $this->spTypes = $writer->getSpTypes(); } break; case DggContainer::class: // this is a container record // initialize $innerData = ''; // write the dgg $recVer = 0x0; $recInstance = 0x0000; $recType = 0xF006; $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; // dgg data $dggData = pack( 'VVVV', $this->object->getSpIdMax(), // maximum shape identifier increased by one $this->object->getCDgSaved() + 1, // number of file identifier clusters increased by one $this->object->getCSpSaved(), $this->object->getCDgSaved() // count total number of drawings saved ); // add file identifier clusters (one per drawing) $IDCLs = $this->object->getIDCLs(); foreach ($IDCLs as $dgId => $maxReducedSpId) { $dggData .= pack('VV', $dgId, $maxReducedSpId + 1); } $header = pack('vvV', $recVerInstance, $recType, strlen($dggData)); $innerData .= $header . $dggData; // write the bstoreContainer if ($bstoreContainer = $this->object->getBstoreContainer()) { $writer = new self($bstoreContainer); $innerData .= $writer->close(); } // write the record $recVer = 0xF; $recInstance = 0x0000; $recType = 0xF000; $length = strlen($innerData); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $this->data = $header . $innerData; break; case BstoreContainer::class: // this is a container record // initialize $innerData = ''; // treat the inner data if ($BSECollection = $this->object->getBSECollection()) { foreach ($BSECollection as $BSE) { $writer = new self($BSE); $innerData .= $writer->close(); } } // write the record $recVer = 0xF; $recInstance = count($this->object->getBSECollection()); $recType = 0xF001; $length = strlen($innerData); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $this->data = $header . $innerData; break; case BSE::class: // this is a semi-container record // initialize $innerData = ''; // here we treat the inner data if ($blip = $this->object->getBlip()) { $writer = new self($blip); $innerData .= $writer->close(); } // initialize $data = ''; $btWin32 = $this->object->getBlipType(); $btMacOS = $this->object->getBlipType(); $data .= pack('CC', $btWin32, $btMacOS); $rgbUid = pack('VVVV', 0, 0, 0, 0); // todo $data .= $rgbUid; $tag = 0; $size = strlen($innerData); $cRef = 1; $foDelay = 0; //todo $unused1 = 0x0; $cbName = 0x0; $unused2 = 0x0; $unused3 = 0x0; $data .= pack('vVVVCCCC', $tag, $size, $cRef, $foDelay, $unused1, $cbName, $unused2, $unused3); $data .= $innerData; // write the record $recVer = 0x2; $recInstance = $this->object->getBlipType(); $recType = 0xF007; $length = strlen($data); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $this->data = $header; $this->data .= $data; break; case Blip::class: // this is an atom record // write the record switch ($this->object->getParent()->getBlipType()) { case BSE::BLIPTYPE_JPEG: // initialize $innerData = ''; $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo $innerData .= $rgbUid1; $tag = 0xFF; // todo $innerData .= pack('C', $tag); $innerData .= $this->object->getData(); $recVer = 0x0; $recInstance = 0x46A; $recType = 0xF01D; $length = strlen($innerData); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $this->data = $header; $this->data .= $innerData; break; case BSE::BLIPTYPE_PNG: // initialize $innerData = ''; $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo $innerData .= $rgbUid1; $tag = 0xFF; // todo $innerData .= pack('C', $tag); $innerData .= $this->object->getData(); $recVer = 0x0; $recInstance = 0x6E0; $recType = 0xF01E; $length = strlen($innerData); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $this->data = $header; $this->data .= $innerData; break; } break; case DgContainer::class: // this is a container record // initialize $innerData = ''; // write the dg $recVer = 0x0; $recInstance = $this->object->getDgId(); $recType = 0xF008; $length = 8; $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); // number of shapes in this drawing (including group shape) $countShapes = count($this->object->getSpgrContainer()->getChildren()); $innerData .= $header . pack('VV', $countShapes, $this->object->getLastSpId()); // write the spgrContainer if ($spgrContainer = $this->object->getSpgrContainer()) { $writer = new self($spgrContainer); $innerData .= $writer->close(); // get the shape offsets relative to the spgrContainer record $spOffsets = $writer->getSpOffsets(); $spTypes = $writer->getSpTypes(); // save the shape offsets relative to dgContainer foreach ($spOffsets as &$spOffset) { $spOffset += 24; // add length of dgContainer header data (8 bytes) plus dg data (16 bytes) } $this->spOffsets = $spOffsets; $this->spTypes = $spTypes; } // write the record $recVer = 0xF; $recInstance = 0x0000; $recType = 0xF002; $length = strlen($innerData); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $this->data = $header . $innerData; break; case SpgrContainer::class: // this is a container record // initialize $innerData = ''; // initialize spape offsets $totalSize = 8; $spOffsets = []; $spTypes = []; // treat the inner data foreach ($this->object->getChildren() as $spContainer) { $writer = new self($spContainer); $spData = $writer->close(); $innerData .= $spData; // save the shape offsets (where new shape records begin) $totalSize += strlen($spData); $spOffsets[] = $totalSize; $spTypes = array_merge($spTypes, $writer->getSpTypes()); } // write the record $recVer = 0xF; $recInstance = 0x0000; $recType = 0xF003; $length = strlen($innerData); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $this->data = $header . $innerData; $this->spOffsets = $spOffsets; $this->spTypes = $spTypes; break; case SpContainer::class: // initialize $data = ''; // build the data // write group shape record, if necessary? if ($this->object->getSpgr()) { $recVer = 0x1; $recInstance = 0x0000; $recType = 0xF009; $length = 0x00000010; $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $data .= $header . pack('VVVV', 0, 0, 0, 0); } $this->spTypes[] = ($this->object->getSpType()); // write the shape record $recVer = 0x2; $recInstance = $this->object->getSpType(); // shape type $recType = 0xF00A; $length = 0x00000008; $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $data .= $header . pack('VV', $this->object->getSpId(), $this->object->getSpgr() ? 0x0005 : 0x0A00); // the options if ($this->object->getOPTCollection()) { $optData = ''; $recVer = 0x3; $recInstance = count($this->object->getOPTCollection()); $recType = 0xF00B; foreach ($this->object->getOPTCollection() as $property => $value) { $optData .= pack('vV', $property, $value); } $length = strlen($optData); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $data .= $header . $optData; } // the client anchor if ($this->object->getStartCoordinates()) { $clientAnchorData = ''; $recVer = 0x0; $recInstance = 0x0; $recType = 0xF010; // start coordinates [$column, $row] = Coordinate::indexesFromString($this->object->getStartCoordinates()); $c1 = $column - 1; $r1 = $row - 1; // start offsetX $startOffsetX = $this->object->getStartOffsetX(); // start offsetY $startOffsetY = $this->object->getStartOffsetY(); // end coordinates [$column, $row] = Coordinate::indexesFromString($this->object->getEndCoordinates()); $c2 = $column - 1; $r2 = $row - 1; // end offsetX $endOffsetX = $this->object->getEndOffsetX(); // end offsetY $endOffsetY = $this->object->getEndOffsetY(); $clientAnchorData = pack('vvvvvvvvv', $this->object->getSpFlag(), $c1, $startOffsetX, $r1, $startOffsetY, $c2, $endOffsetX, $r2, $endOffsetY); $length = strlen($clientAnchorData); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $data .= $header . $clientAnchorData; } // the client data, just empty for now if (!$this->object->getSpgr()) { $clientDataData = ''; $recVer = 0x0; $recInstance = 0x0; $recType = 0xF011; $length = strlen($clientDataData); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $data .= $header . $clientDataData; } // write the record $recVer = 0xF; $recInstance = 0x0000; $recType = 0xF004; $length = strlen($data); $recVerInstance = $recVer; $recVerInstance |= $recInstance << 4; $header = pack('vvV', $recVerInstance, $recType, $length); $this->data = $header . $data; break; } return $this->data; } /** * Gets the shape offsets. * * @return array */ public function getSpOffsets() { return $this->spOffsets; } /** * Gets the shape types. * * @return array */ public function getSpTypes() { return $this->spTypes; } } PK[?!Style/CellFont.phpnu[setSuperscript(true); break; case 0x0002: $font->setSubscript(true); break; } } /** * @var array */ protected static $underlineMap = [ 0x01 => Font::UNDERLINE_SINGLE, 0x02 => Font::UNDERLINE_DOUBLE, 0x21 => Font::UNDERLINE_SINGLEACCOUNTING, 0x22 => Font::UNDERLINE_DOUBLEACCOUNTING, ]; public static function underline(Font $font, int $underline): void { if (array_key_exists($underline, self::$underlineMap)) { $font->setUnderline(self::$underlineMap[$underline]); } } } PK[[HHStyle/Border.phpnu[ */ protected static $borderStyleMap = [ 0x00 => StyleBorder::BORDER_NONE, 0x01 => StyleBorder::BORDER_THIN, 0x02 => StyleBorder::BORDER_MEDIUM, 0x03 => StyleBorder::BORDER_DASHED, 0x04 => StyleBorder::BORDER_DOTTED, 0x05 => StyleBorder::BORDER_THICK, 0x06 => StyleBorder::BORDER_DOUBLE, 0x07 => StyleBorder::BORDER_HAIR, 0x08 => StyleBorder::BORDER_MEDIUMDASHED, 0x09 => StyleBorder::BORDER_DASHDOT, 0x0A => StyleBorder::BORDER_MEDIUMDASHDOT, 0x0B => StyleBorder::BORDER_DASHDOTDOT, 0x0C => StyleBorder::BORDER_MEDIUMDASHDOTDOT, 0x0D => StyleBorder::BORDER_SLANTDASHDOT, ]; public static function lookup(int $index): string { if (isset(self::$borderStyleMap[$index])) { return self::$borderStyleMap[$index]; } return StyleBorder::BORDER_NONE; } } PK[uStyle/FillPattern.phpnu[ */ protected static $fillPatternMap = [ 0x00 => Fill::FILL_NONE, 0x01 => Fill::FILL_SOLID, 0x02 => Fill::FILL_PATTERN_MEDIUMGRAY, 0x03 => Fill::FILL_PATTERN_DARKGRAY, 0x04 => Fill::FILL_PATTERN_LIGHTGRAY, 0x05 => Fill::FILL_PATTERN_DARKHORIZONTAL, 0x06 => Fill::FILL_PATTERN_DARKVERTICAL, 0x07 => Fill::FILL_PATTERN_DARKDOWN, 0x08 => Fill::FILL_PATTERN_DARKUP, 0x09 => Fill::FILL_PATTERN_DARKGRID, 0x0A => Fill::FILL_PATTERN_DARKTRELLIS, 0x0B => Fill::FILL_PATTERN_LIGHTHORIZONTAL, 0x0C => Fill::FILL_PATTERN_LIGHTVERTICAL, 0x0D => Fill::FILL_PATTERN_LIGHTDOWN, 0x0E => Fill::FILL_PATTERN_LIGHTUP, 0x0F => Fill::FILL_PATTERN_LIGHTGRID, 0x10 => Fill::FILL_PATTERN_LIGHTTRELLIS, 0x11 => Fill::FILL_PATTERN_GRAY125, 0x12 => Fill::FILL_PATTERN_GRAY0625, ]; /** * Get fill pattern from index * OpenOffice documentation: 2.5.12. * * @param int $index * * @return string */ public static function lookup($index) { if (isset(self::$fillPatternMap[$index])) { return self::$fillPatternMap[$index]; } return Fill::FILL_NONE; } } PK[YÁStyle/CellAlignment.phpnu[ */ private static $horizontalMap = [ Alignment::HORIZONTAL_GENERAL => 0, Alignment::HORIZONTAL_LEFT => 1, Alignment::HORIZONTAL_RIGHT => 3, Alignment::HORIZONTAL_CENTER => 2, Alignment::HORIZONTAL_CENTER_CONTINUOUS => 6, Alignment::HORIZONTAL_JUSTIFY => 5, ]; /** * @var array */ private static $verticalMap = [ Alignment::VERTICAL_BOTTOM => 2, Alignment::VERTICAL_TOP => 0, Alignment::VERTICAL_CENTER => 1, Alignment::VERTICAL_JUSTIFY => 3, ]; public static function horizontal(Alignment $alignment): int { $horizontalAlignment = $alignment->getHorizontal(); if (is_string($horizontalAlignment) && array_key_exists($horizontalAlignment, self::$horizontalMap)) { return self::$horizontalMap[$horizontalAlignment]; } return self::$horizontalMap[Alignment::HORIZONTAL_GENERAL]; } public static function wrap(Alignment $alignment): int { $wrap = $alignment->getWrapText(); return ($wrap === true) ? 1 : 0; } public static function vertical(Alignment $alignment): int { $verticalAlignment = $alignment->getVertical(); if (is_string($verticalAlignment) && array_key_exists($verticalAlignment, self::$verticalMap)) { return self::$verticalMap[$verticalAlignment]; } return self::$verticalMap[Alignment::VERTICAL_BOTTOM]; } } PK[s|d CellDataValidation.phpnu[ */ protected static $validationTypeMap = [ DataValidation::TYPE_NONE => 0x00, DataValidation::TYPE_WHOLE => 0x01, DataValidation::TYPE_DECIMAL => 0x02, DataValidation::TYPE_LIST => 0x03, DataValidation::TYPE_DATE => 0x04, DataValidation::TYPE_TIME => 0x05, DataValidation::TYPE_TEXTLENGTH => 0x06, DataValidation::TYPE_CUSTOM => 0x07, ]; /** * @var array */ protected static $errorStyleMap = [ DataValidation::STYLE_STOP => 0x00, DataValidation::STYLE_WARNING => 0x01, DataValidation::STYLE_INFORMATION => 0x02, ]; /** * @var array */ protected static $operatorMap = [ DataValidation::OPERATOR_BETWEEN => 0x00, DataValidation::OPERATOR_NOTBETWEEN => 0x01, DataValidation::OPERATOR_EQUAL => 0x02, DataValidation::OPERATOR_NOTEQUAL => 0x03, DataValidation::OPERATOR_GREATERTHAN => 0x04, DataValidation::OPERATOR_LESSTHAN => 0x05, DataValidation::OPERATOR_GREATERTHANOREQUAL => 0x06, DataValidation::OPERATOR_LESSTHANOREQUAL => 0x07, ]; public static function type(DataValidation $dataValidation): int { $validationType = $dataValidation->getType(); if (is_string($validationType) && array_key_exists($validationType, self::$validationTypeMap)) { return self::$validationTypeMap[$validationType]; } return self::$validationTypeMap[DataValidation::TYPE_NONE]; } public static function errorStyle(DataValidation $dataValidation): int { $errorStyle = $dataValidation->getErrorStyle(); if (is_string($errorStyle) && array_key_exists($errorStyle, self::$errorStyleMap)) { return self::$errorStyleMap[$errorStyle]; } return self::$errorStyleMap[DataValidation::STYLE_STOP]; } public static function operator(DataValidation $dataValidation): int { $operator = $dataValidation->getOperator(); if (is_string($operator) && array_key_exists($operator, self::$operatorMap)) { return self::$operatorMap[$operator]; } return self::$operatorMap[DataValidation::OPERATOR_BETWEEN]; } } PK[؜ Worksheet.phpnu[ // * // * The majority of this is _NOT_ my code. I simply ported it from the // * PERL Spreadsheet::WriteExcel module. // * // * The author of the Spreadsheet::WriteExcel module is John McNamara // * // * // * I _DO_ maintain this code, and John McNamara has nothing to do with the // * porting of this code to PHP. Any questions directly related to this // * class library should be directed to me. // * // * License Information: // * // * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets // * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com // * // * This library is free software; you can redistribute it and/or // * modify it under the terms of the GNU Lesser General Public // * License as published by the Free Software Foundation; either // * version 2.1 of the License, or (at your option) any later version. // * // * This library is distributed in the hope that it will be useful, // * but WITHOUT ANY WARRANTY; without even the implied warranty of // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // * Lesser General Public License for more details. // * // * You should have received a copy of the GNU Lesser General Public // * License along with this library; if not, write to the Free Software // * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // */ class Worksheet extends BIFFwriter { /** * Formula parser. * * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser */ private $parser; /** * Maximum number of characters for a string (LABEL record in BIFF5). * * @var int */ private $xlsStringMaxLength; /** * Array containing format information for columns. * * @var array */ private $columnInfo; /** * Array containing the selected area for the worksheet. * * @var array */ private $selection; /** * The active pane for the worksheet. * * @var int */ private $activePane; /** * Whether to use outline. * * @var bool */ private $outlineOn; /** * Auto outline styles. * * @var bool */ private $outlineStyle; /** * Whether to have outline summary below. * * @var bool */ private $outlineBelow; /** * Whether to have outline summary at the right. * * @var bool */ private $outlineRight; /** * Reference to the total number of strings in the workbook. * * @var int */ private $stringTotal; /** * Reference to the number of unique strings in the workbook. * * @var int */ private $stringUnique; /** * Reference to the array containing all the unique strings in the workbook. * * @var array */ private $stringTable; /** * Color cache. */ private $colors; /** * Index of first used row (at least 0). * * @var int */ private $firstRowIndex; /** * Index of last used row. (no used rows means -1). * * @var int */ private $lastRowIndex; /** * Index of first used column (at least 0). * * @var int */ private $firstColumnIndex; /** * Index of last used column (no used columns means -1). * * @var int */ private $lastColumnIndex; /** * Sheet object. * * @var \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet */ public $phpSheet; /** * Count cell style Xfs. * * @var int */ private $countCellStyleXfs; /** * Escher object corresponding to MSODRAWING. * * @var null|\PhpOffice\PhpSpreadsheet\Shared\Escher */ private $escher; /** * Array of font hashes associated to FONT records index. * * @var array */ public $fontHashIndex; /** * @var bool */ private $preCalculateFormulas; /** * @var int */ private $printHeaders; /** * Constructor. * * @param int $str_total Total number of strings * @param int $str_unique Total number of unique strings * @param array $str_table String Table * @param array $colors Colour Table * @param Parser $parser The formula parser created for the Workbook * @param bool $preCalculateFormulas Flag indicating whether formulas should be calculated or just written * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet The worksheet to write */ public function __construct(&$str_total, &$str_unique, &$str_table, &$colors, Parser $parser, $preCalculateFormulas, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet) { // It needs to call its parent's constructor explicitly parent::__construct(); $this->preCalculateFormulas = $preCalculateFormulas; $this->stringTotal = &$str_total; $this->stringUnique = &$str_unique; $this->stringTable = &$str_table; $this->colors = &$colors; $this->parser = $parser; $this->phpSheet = $phpSheet; $this->xlsStringMaxLength = 255; $this->columnInfo = []; $this->selection = [0, 0, 0, 0]; $this->activePane = 3; $this->printHeaders = 0; $this->outlineStyle = false; $this->outlineBelow = true; $this->outlineRight = true; $this->outlineOn = true; $this->fontHashIndex = []; // calculate values for DIMENSIONS record $minR = 1; $minC = 'A'; $maxR = $this->phpSheet->getHighestRow(); $maxC = $this->phpSheet->getHighestColumn(); // Determine lowest and highest column and row $this->firstRowIndex = $minR; $this->lastRowIndex = ($maxR > 65535) ? 65535 : $maxR; $this->firstColumnIndex = Coordinate::columnIndexFromString($minC); $this->lastColumnIndex = Coordinate::columnIndexFromString($maxC); if ($this->lastColumnIndex > 255) { $this->lastColumnIndex = 255; } $this->countCellStyleXfs = count($phpSheet->getParent()->getCellStyleXfCollection()); } /** * Add data to the beginning of the workbook (note the reverse order) * and to the end of the workbook. * * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::storeWorkbook() */ public function close(): void { $phpSheet = $this->phpSheet; // Storing selected cells and active sheet because it changes while parsing cells with formulas. $selectedCells = $this->phpSheet->getSelectedCells(); $activeSheetIndex = $this->phpSheet->getParent()->getActiveSheetIndex(); // Write BOF record $this->storeBof(0x0010); // Write PRINTHEADERS $this->writePrintHeaders(); // Write PRINTGRIDLINES $this->writePrintGridlines(); // Write GRIDSET $this->writeGridset(); // Calculate column widths $phpSheet->calculateColumnWidths(); // Column dimensions if (($defaultWidth = $phpSheet->getDefaultColumnDimension()->getWidth()) < 0) { $defaultWidth = \PhpOffice\PhpSpreadsheet\Shared\Font::getDefaultColumnWidthByFont($phpSheet->getParent()->getDefaultStyle()->getFont()); } $columnDimensions = $phpSheet->getColumnDimensions(); $maxCol = $this->lastColumnIndex - 1; for ($i = 0; $i <= $maxCol; ++$i) { $hidden = 0; $level = 0; $xfIndex = 15; // there are 15 cell style Xfs $width = $defaultWidth; $columnLetter = Coordinate::stringFromColumnIndex($i + 1); if (isset($columnDimensions[$columnLetter])) { $columnDimension = $columnDimensions[$columnLetter]; if ($columnDimension->getWidth() >= 0) { $width = $columnDimension->getWidth(); } $hidden = $columnDimension->getVisible() ? 0 : 1; $level = $columnDimension->getOutlineLevel(); $xfIndex = $columnDimension->getXfIndex() + 15; // there are 15 cell style Xfs } // Components of columnInfo: // $firstcol first column on the range // $lastcol last column on the range // $width width to set // $xfIndex The optional cell style Xf index to apply to the columns // $hidden The optional hidden atribute // $level The optional outline level $this->columnInfo[] = [$i, $i, $width, $xfIndex, $hidden, $level]; } // Write GUTS $this->writeGuts(); // Write DEFAULTROWHEIGHT $this->writeDefaultRowHeight(); // Write WSBOOL $this->writeWsbool(); // Write horizontal and vertical page breaks $this->writeBreaks(); // Write page header $this->writeHeader(); // Write page footer $this->writeFooter(); // Write page horizontal centering $this->writeHcenter(); // Write page vertical centering $this->writeVcenter(); // Write left margin $this->writeMarginLeft(); // Write right margin $this->writeMarginRight(); // Write top margin $this->writeMarginTop(); // Write bottom margin $this->writeMarginBottom(); // Write page setup $this->writeSetup(); // Write sheet protection $this->writeProtect(); // Write SCENPROTECT $this->writeScenProtect(); // Write OBJECTPROTECT $this->writeObjectProtect(); // Write sheet password $this->writePassword(); // Write DEFCOLWIDTH record $this->writeDefcol(); // Write the COLINFO records if they exist if (!empty($this->columnInfo)) { $colcount = count($this->columnInfo); for ($i = 0; $i < $colcount; ++$i) { $this->writeColinfo($this->columnInfo[$i]); } } $autoFilterRange = $phpSheet->getAutoFilter()->getRange(); if (!empty($autoFilterRange)) { // Write AUTOFILTERINFO $this->writeAutoFilterInfo(); } // Write sheet dimensions $this->writeDimensions(); // Row dimensions foreach ($phpSheet->getRowDimensions() as $rowDimension) { $xfIndex = $rowDimension->getXfIndex() + 15; // there are 15 cellXfs $this->writeRow( $rowDimension->getRowIndex() - 1, (int) $rowDimension->getRowHeight(), $xfIndex, !$rowDimension->getVisible(), $rowDimension->getOutlineLevel() ); } // Write Cells foreach ($phpSheet->getCoordinates() as $coordinate) { $cell = $phpSheet->getCell($coordinate); $row = $cell->getRow() - 1; $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1; // Don't break Excel break the code! if ($row > 65535 || $column > 255) { throw new WriterException('Rows or columns overflow! Excel5 has limit to 65535 rows and 255 columns. Use XLSX instead.'); } // Write cell value $xfIndex = $cell->getXfIndex() + 15; // there are 15 cell style Xfs $cVal = $cell->getValue(); if ($cVal instanceof RichText) { $arrcRun = []; $str_pos = 0; $elements = $cVal->getRichTextElements(); foreach ($elements as $element) { // FONT Index if ($element instanceof Run) { $str_fontidx = $this->fontHashIndex[$element->getFont()->getHashCode()]; } else { $str_fontidx = 0; } $arrcRun[] = ['strlen' => $str_pos, 'fontidx' => $str_fontidx]; // Position FROM $str_pos += StringHelper::countCharacters($element->getText(), 'UTF-8'); } $this->writeRichTextString($row, $column, $cVal->getPlainText(), $xfIndex, $arrcRun); } else { switch ($cell->getDatatype()) { case DataType::TYPE_STRING: case DataType::TYPE_NULL: if ($cVal === '' || $cVal === null) { $this->writeBlank($row, $column, $xfIndex); } else { $this->writeString($row, $column, $cVal, $xfIndex); } break; case DataType::TYPE_NUMERIC: $this->writeNumber($row, $column, $cVal, $xfIndex); break; case DataType::TYPE_FORMULA: $calculatedValue = $this->preCalculateFormulas ? $cell->getCalculatedValue() : null; if (self::WRITE_FORMULA_EXCEPTION == $this->writeFormula($row, $column, $cVal, $xfIndex, $calculatedValue)) { if ($calculatedValue === null) { $calculatedValue = $cell->getCalculatedValue(); } $calctype = gettype($calculatedValue); switch ($calctype) { case 'integer': case 'double': $this->writeNumber($row, $column, $calculatedValue, $xfIndex); break; case 'string': $this->writeString($row, $column, $calculatedValue, $xfIndex); break; case 'boolean': $this->writeBoolErr($row, $column, $calculatedValue, 0, $xfIndex); break; default: $this->writeString($row, $column, $cVal, $xfIndex); } } break; case DataType::TYPE_BOOL: $this->writeBoolErr($row, $column, $cVal, 0, $xfIndex); break; case DataType::TYPE_ERROR: $this->writeBoolErr($row, $column, ErrorCode::error($cVal), 1, $xfIndex); break; } } } // Append $this->writeMsoDrawing(); // Restoring active sheet. $this->phpSheet->getParent()->setActiveSheetIndex($activeSheetIndex); // Write WINDOW2 record $this->writeWindow2(); // Write PLV record $this->writePageLayoutView(); // Write ZOOM record $this->writeZoom(); if ($phpSheet->getFreezePane()) { $this->writePanes(); } // Restoring selected cells. $this->phpSheet->setSelectedCells($selectedCells); // Write SELECTION record $this->writeSelection(); // Write MergedCellsTable Record $this->writeMergedCells(); // Hyperlinks foreach ($phpSheet->getHyperLinkCollection() as $coordinate => $hyperlink) { [$column, $row] = Coordinate::indexesFromString($coordinate); $url = $hyperlink->getUrl(); if (strpos($url, 'sheet://') !== false) { // internal to current workbook $url = str_replace('sheet://', 'internal:', $url); } elseif (preg_match('/^(http:|https:|ftp:|mailto:)/', $url)) { // URL } else { // external (local file) $url = 'external:' . $url; } $this->writeUrl($row - 1, $column - 1, $url); } $this->writeDataValidity(); $this->writeSheetLayout(); // Write SHEETPROTECTION record $this->writeSheetProtection(); $this->writeRangeProtection(); $arrConditionalStyles = $phpSheet->getConditionalStylesCollection(); if (!empty($arrConditionalStyles)) { $arrConditional = []; $cfHeaderWritten = false; // Write ConditionalFormattingTable records foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) { foreach ($conditionalStyles as $conditional) { /** @var Conditional $conditional */ if ( $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION || $conditional->getConditionType() == Conditional::CONDITION_CELLIS ) { // Write CFHEADER record (only if there are Conditional Styles that we are able to write) if ($cfHeaderWritten === false) { $this->writeCFHeader(); $cfHeaderWritten = true; } if (!isset($arrConditional[$conditional->getHashCode()])) { // This hash code has been handled $arrConditional[$conditional->getHashCode()] = true; // Write CFRULE record $this->writeCFRule($conditional); } } } } } $this->storeEof(); } /** * Write a cell range address in BIFF8 * always fixed range * See section 2.5.14 in OpenOffice.org's Documentation of the Microsoft Excel File Format. * * @param string $range E.g. 'A1' or 'A1:B6' * * @return string Binary data */ private function writeBIFF8CellRangeAddressFixed($range) { $explodes = explode(':', $range); // extract first cell, e.g. 'A1' $firstCell = $explodes[0]; // extract last cell, e.g. 'B6' if (count($explodes) == 1) { $lastCell = $firstCell; } else { $lastCell = $explodes[1]; } $firstCellCoordinates = Coordinate::indexesFromString($firstCell); // e.g. [0, 1] $lastCellCoordinates = Coordinate::indexesFromString($lastCell); // e.g. [1, 6] return pack('vvvv', $firstCellCoordinates[1] - 1, $lastCellCoordinates[1] - 1, $firstCellCoordinates[0] - 1, $lastCellCoordinates[0] - 1); } /** * Retrieves data from memory in one chunk, or from disk * sized chunks. * * @return string The data */ public function getData() { // Return data stored in memory if (isset($this->_data)) { $tmp = $this->_data; $this->_data = null; return $tmp; } // No data to return return ''; } /** * Set the option to print the row and column headers on the printed page. * * @param int $print Whether to print the headers or not. Defaults to 1 (print). */ public function printRowColHeaders($print = 1): void { $this->printHeaders = $print; } /** * This method sets the properties for outlining and grouping. The defaults * correspond to Excel's defaults. * * @param bool $visible * @param bool $symbols_below * @param bool $symbols_right * @param bool $auto_style */ public function setOutline($visible = true, $symbols_below = true, $symbols_right = true, $auto_style = false): void { $this->outlineOn = $visible; $this->outlineBelow = $symbols_below; $this->outlineRight = $symbols_right; $this->outlineStyle = $auto_style; } /** * Write a double to the specified row and column (zero indexed). * An integer can be written as a double. Excel will display an * integer. $format is optional. * * Returns 0 : normal termination * -2 : row or column out of range * * @param int $row Zero indexed row * @param int $col Zero indexed column * @param float $num The number to write * @param mixed $xfIndex The optional XF format * * @return int */ private function writeNumber($row, $col, $num, $xfIndex) { $record = 0x0203; // Record identifier $length = 0x000E; // Number of bytes to follow $header = pack('vv', $record, $length); $data = pack('vvv', $row, $col, $xfIndex); $xl_double = pack('d', $num); if (self::getByteOrder()) { // if it's Big Endian $xl_double = strrev($xl_double); } $this->append($header . $data . $xl_double); return 0; } /** * Write a LABELSST record or a LABEL record. Which one depends on BIFF version. * * @param int $row Row index (0-based) * @param int $col Column index (0-based) * @param string $str The string * @param int $xfIndex Index to XF record */ private function writeString($row, $col, $str, $xfIndex): void { $this->writeLabelSst($row, $col, $str, $xfIndex); } /** * Write a LABELSST record or a LABEL record. Which one depends on BIFF version * It differs from writeString by the writing of rich text strings. * * @param int $row Row index (0-based) * @param int $col Column index (0-based) * @param string $str The string * @param int $xfIndex The XF format index for the cell * @param array $arrcRun Index to Font record and characters beginning */ private function writeRichTextString($row, $col, $str, $xfIndex, $arrcRun): void { $record = 0x00FD; // Record identifier $length = 0x000A; // Bytes to follow $str = StringHelper::UTF8toBIFF8UnicodeShort($str, $arrcRun); // check if string is already present if (!isset($this->stringTable[$str])) { $this->stringTable[$str] = $this->stringUnique++; } ++$this->stringTotal; $header = pack('vv', $record, $length); $data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]); $this->append($header . $data); } /** * Write a string to the specified row and column (zero indexed). * This is the BIFF8 version (no 255 chars limit). * $format is optional. * * @param int $row Zero indexed row * @param int $col Zero indexed column * @param string $str The string to write * @param mixed $xfIndex The XF format index for the cell */ private function writeLabelSst($row, $col, $str, $xfIndex): void { $record = 0x00FD; // Record identifier $length = 0x000A; // Bytes to follow $str = StringHelper::UTF8toBIFF8UnicodeLong($str); // check if string is already present if (!isset($this->stringTable[$str])) { $this->stringTable[$str] = $this->stringUnique++; } ++$this->stringTotal; $header = pack('vv', $record, $length); $data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]); $this->append($header . $data); } /** * Write a blank cell to the specified row and column (zero indexed). * A blank cell is used to specify formatting without adding a string * or a number. * * A blank cell without a format serves no purpose. Therefore, we don't write * a BLANK record unless a format is specified. * * Returns 0 : normal termination (including no format) * -1 : insufficient number of arguments * -2 : row or column out of range * * @param int $row Zero indexed row * @param int $col Zero indexed column * @param mixed $xfIndex The XF format index * * @return int */ public function writeBlank($row, $col, $xfIndex) { $record = 0x0201; // Record identifier $length = 0x0006; // Number of bytes to follow $header = pack('vv', $record, $length); $data = pack('vvv', $row, $col, $xfIndex); $this->append($header . $data); return 0; } /** * Write a boolean or an error type to the specified row and column (zero indexed). * * @param int $row Row index (0-based) * @param int $col Column index (0-based) * @param int $value * @param bool $isError Error or Boolean? * @param int $xfIndex * * @return int */ private function writeBoolErr($row, $col, $value, $isError, $xfIndex) { $record = 0x0205; $length = 8; $header = pack('vv', $record, $length); $data = pack('vvvCC', $row, $col, $xfIndex, $value, $isError); $this->append($header . $data); return 0; } const WRITE_FORMULA_NORMAL = 0; const WRITE_FORMULA_ERRORS = -1; const WRITE_FORMULA_RANGE = -2; const WRITE_FORMULA_EXCEPTION = -3; /** * Write a formula to the specified row and column (zero indexed). * The textual representation of the formula is passed to the parser in * Parser.php which returns a packed binary string. * * Returns 0 : WRITE_FORMULA_NORMAL normal termination * -1 : WRITE_FORMULA_ERRORS formula errors (bad formula) * -2 : WRITE_FORMULA_RANGE row or column out of range * -3 : WRITE_FORMULA_EXCEPTION parse raised exception, probably due to definedname * * @param int $row Zero indexed row * @param int $col Zero indexed column * @param string $formula The formula text string * @param mixed $xfIndex The XF format index * @param mixed $calculatedValue Calculated value * * @return int */ private function writeFormula($row, $col, $formula, $xfIndex, $calculatedValue) { $record = 0x0006; // Record identifier // Initialize possible additional value for STRING record that should be written after the FORMULA record? $stringValue = null; // calculated value if (isset($calculatedValue)) { // Since we can't yet get the data type of the calculated value, // we use best effort to determine data type if (is_bool($calculatedValue)) { // Boolean value $num = pack('CCCvCv', 0x01, 0x00, (int) $calculatedValue, 0x00, 0x00, 0xFFFF); } elseif (is_int($calculatedValue) || is_float($calculatedValue)) { // Numeric value $num = pack('d', $calculatedValue); } elseif (is_string($calculatedValue)) { $errorCodes = DataType::getErrorCodes(); if (isset($errorCodes[$calculatedValue])) { // Error value $num = pack('CCCvCv', 0x02, 0x00, ErrorCode::error($calculatedValue), 0x00, 0x00, 0xFFFF); } elseif ($calculatedValue === '') { // Empty string (and BIFF8) $num = pack('CCCvCv', 0x03, 0x00, 0x00, 0x00, 0x00, 0xFFFF); } else { // Non-empty string value (or empty string BIFF5) $stringValue = $calculatedValue; $num = pack('CCCvCv', 0x00, 0x00, 0x00, 0x00, 0x00, 0xFFFF); } } else { // We are really not supposed to reach here $num = pack('d', 0x00); } } else { $num = pack('d', 0x00); } $grbit = 0x03; // Option flags $unknown = 0x0000; // Must be zero // Strip the '=' or '@' sign at the beginning of the formula string if ($formula[0] == '=') { $formula = substr($formula, 1); } else { // Error handling $this->writeString($row, $col, 'Unrecognised character for formula', 0); return self::WRITE_FORMULA_ERRORS; } // Parse the formula using the parser in Parser.php try { $this->parser->parse($formula); $formula = $this->parser->toReversePolish(); $formlen = strlen($formula); // Length of the binary string $length = 0x16 + $formlen; // Length of the record data $header = pack('vv', $record, $length); $data = pack('vvv', $row, $col, $xfIndex) . $num . pack('vVv', $grbit, $unknown, $formlen); $this->append($header . $data . $formula); // Append also a STRING record if necessary if ($stringValue !== null) { $this->writeStringRecord($stringValue); } return self::WRITE_FORMULA_NORMAL; } catch (PhpSpreadsheetException $e) { return self::WRITE_FORMULA_EXCEPTION; } } /** * Write a STRING record. This. * * @param string $stringValue */ private function writeStringRecord($stringValue): void { $record = 0x0207; // Record identifier $data = StringHelper::UTF8toBIFF8UnicodeLong($stringValue); $length = strlen($data); $header = pack('vv', $record, $length); $this->append($header . $data); } /** * Write a hyperlink. * This is comprised of two elements: the visible label and * the invisible link. The visible label is the same as the link unless an * alternative string is specified. The label is written using the * writeString() method. Therefore the 255 characters string limit applies. * $string and $format are optional. * * The hyperlink can be to a http, ftp, mail, internal sheet (not yet), or external * directory url. * * @param int $row Row * @param int $col Column * @param string $url URL string */ private function writeUrl($row, $col, $url): void { // Add start row and col to arg list $this->writeUrlRange($row, $col, $row, $col, $url); } /** * This is the more general form of writeUrl(). It allows a hyperlink to be * written to a range of cells. This function also decides the type of hyperlink * to be written. These are either, Web (http, ftp, mailto), Internal * (Sheet1!A1) or external ('c:\temp\foo.xls#Sheet1!A1'). * * @param int $row1 Start row * @param int $col1 Start column * @param int $row2 End row * @param int $col2 End column * @param string $url URL string * * @see writeUrl() */ private function writeUrlRange($row1, $col1, $row2, $col2, $url): void { // Check for internal/external sheet links or default to web link if (preg_match('[^internal:]', $url)) { $this->writeUrlInternal($row1, $col1, $row2, $col2, $url); } if (preg_match('[^external:]', $url)) { $this->writeUrlExternal($row1, $col1, $row2, $col2, $url); } $this->writeUrlWeb($row1, $col1, $row2, $col2, $url); } /** * Used to write http, ftp and mailto hyperlinks. * The link type ($options) is 0x03 is the same as absolute dir ref without * sheet. However it is differentiated by the $unknown2 data stream. * * @param int $row1 Start row * @param int $col1 Start column * @param int $row2 End row * @param int $col2 End column * @param string $url URL string * * @see writeUrl() */ public function writeUrlWeb($row1, $col1, $row2, $col2, $url): void { $record = 0x01B8; // Record identifier // Pack the undocumented parts of the hyperlink stream $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); $unknown2 = pack('H*', 'E0C9EA79F9BACE118C8200AA004BA90B'); // Pack the option flags $options = pack('V', 0x03); // Convert URL to a null terminated wchar string $url = implode("\0", preg_split("''", $url, -1, PREG_SPLIT_NO_EMPTY)); $url = $url . "\0\0\0"; // Pack the length of the URL $url_len = pack('V', strlen($url)); // Calculate the data length $length = 0x34 + strlen($url); // Pack the header data $header = pack('vv', $record, $length); $data = pack('vvvv', $row1, $row2, $col1, $col2); // Write the packed data $this->append($header . $data . $unknown1 . $options . $unknown2 . $url_len . $url); } /** * Used to write internal reference hyperlinks such as "Sheet1!A1". * * @param int $row1 Start row * @param int $col1 Start column * @param int $row2 End row * @param int $col2 End column * @param string $url URL string * * @see writeUrl() */ private function writeUrlInternal($row1, $col1, $row2, $col2, $url): void { $record = 0x01B8; // Record identifier // Strip URL type $url = preg_replace('/^internal:/', '', $url); // Pack the undocumented parts of the hyperlink stream $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); // Pack the option flags $options = pack('V', 0x08); // Convert the URL type and to a null terminated wchar string $url .= "\0"; // character count $url_len = StringHelper::countCharacters($url); $url_len = pack('V', $url_len); $url = StringHelper::convertEncoding($url, 'UTF-16LE', 'UTF-8'); // Calculate the data length $length = 0x24 + strlen($url); // Pack the header data $header = pack('vv', $record, $length); $data = pack('vvvv', $row1, $row2, $col1, $col2); // Write the packed data $this->append($header . $data . $unknown1 . $options . $url_len . $url); } /** * Write links to external directory names such as 'c:\foo.xls', * c:\foo.xls#Sheet1!A1', '../../foo.xls'. and '../../foo.xls#Sheet1!A1'. * * Note: Excel writes some relative links with the $dir_long string. We ignore * these cases for the sake of simpler code. * * @param int $row1 Start row * @param int $col1 Start column * @param int $row2 End row * @param int $col2 End column * @param string $url URL string * * @see writeUrl() */ private function writeUrlExternal($row1, $col1, $row2, $col2, $url): void { // Network drives are different. We will handle them separately // MS/Novell network drives and shares start with \\ if (preg_match('[^external:\\\\]', $url)) { return; } $record = 0x01B8; // Record identifier $length = 0x00000; // Bytes to follow // Strip URL type and change Unix dir separator to Dos style (if needed) // $url = preg_replace('/^external:/', '', $url); $url = preg_replace('/\//', '\\', $url); // Determine if the link is relative or absolute: // relative if link contains no dir separator, "somefile.xls" // relative if link starts with up-dir, "..\..\somefile.xls" // otherwise, absolute $absolute = 0x00; // relative path if (preg_match('/^[A-Z]:/', $url)) { $absolute = 0x02; // absolute path on Windows, e.g. C:\... } $link_type = 0x01 | $absolute; // Determine if the link contains a sheet reference and change some of the // parameters accordingly. // Split the dir name and sheet name (if it exists) $dir_long = $url; if (preg_match('/\\#/', $url)) { $link_type |= 0x08; } // Pack the link type $link_type = pack('V', $link_type); // Calculate the up-level dir count e.g.. (..\..\..\ == 3) $up_count = preg_match_all('/\\.\\.\\\\/', $dir_long, $useless); $up_count = pack('v', $up_count); // Store the short dos dir name (null terminated) $dir_short = preg_replace('/\\.\\.\\\\/', '', $dir_long) . "\0"; // Store the long dir name as a wchar string (non-null terminated) $dir_long = $dir_long . "\0"; // Pack the lengths of the dir strings $dir_short_len = pack('V', strlen($dir_short)); $dir_long_len = pack('V', strlen($dir_long)); $stream_len = pack('V', 0); //strlen($dir_long) + 0x06); // Pack the undocumented parts of the hyperlink stream $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); $unknown2 = pack('H*', '0303000000000000C000000000000046'); $unknown3 = pack('H*', 'FFFFADDE000000000000000000000000000000000000000'); $unknown4 = pack('v', 0x03); // Pack the main data stream $data = pack('vvvv', $row1, $row2, $col1, $col2) . $unknown1 . $link_type . $unknown2 . $up_count . $dir_short_len . $dir_short . $unknown3 . $stream_len; /*. $dir_long_len . $unknown4 . $dir_long . $sheet_len . $sheet ;*/ // Pack the header data $length = strlen($data); $header = pack('vv', $record, $length); // Write the packed data $this->append($header . $data); } /** * This method is used to set the height and format for a row. * * @param int $row The row to set * @param int $height Height we are giving to the row. * Use null to set XF without setting height * @param int $xfIndex The optional cell style Xf index to apply to the columns * @param bool $hidden The optional hidden attribute * @param int $level The optional outline level for row, in range [0,7] */ private function writeRow($row, $height, $xfIndex, $hidden = false, $level = 0): void { $record = 0x0208; // Record identifier $length = 0x0010; // Number of bytes to follow $colMic = 0x0000; // First defined column $colMac = 0x0000; // Last defined column $irwMac = 0x0000; // Used by Excel to optimise loading $reserved = 0x0000; // Reserved $grbit = 0x0000; // Option flags $ixfe = $xfIndex; if ($height < 0) { $height = null; } // Use writeRow($row, null, $XF) to set XF format without setting height if ($height !== null) { $miyRw = $height * 20; // row height } else { $miyRw = 0xff; // default row height is 256 } // Set the options flags. fUnsynced is used to show that the font and row // heights are not compatible. This is usually the case for WriteExcel. // The collapsed flag 0x10 doesn't seem to be used to indicate that a row // is collapsed. Instead it is used to indicate that the previous row is // collapsed. The zero height flag, 0x20, is used to collapse a row. $grbit |= $level; if ($hidden === true) { $grbit |= 0x0030; } if ($height !== null) { $grbit |= 0x0040; // fUnsynced } if ($xfIndex !== 0xF) { $grbit |= 0x0080; } $grbit |= 0x0100; $header = pack('vv', $record, $length); $data = pack('vvvvvvvv', $row, $colMic, $colMac, $miyRw, $irwMac, $reserved, $grbit, $ixfe); $this->append($header . $data); } /** * Writes Excel DIMENSIONS to define the area in which there is data. */ private function writeDimensions(): void { $record = 0x0200; // Record identifier $length = 0x000E; $data = pack('VVvvv', $this->firstRowIndex, $this->lastRowIndex + 1, $this->firstColumnIndex, $this->lastColumnIndex + 1, 0x0000); // reserved $header = pack('vv', $record, $length); $this->append($header . $data); } /** * Write BIFF record Window2. */ private function writeWindow2(): void { $record = 0x023E; // Record identifier $length = 0x0012; $grbit = 0x00B6; // Option flags $rwTop = 0x0000; // Top row visible in window $colLeft = 0x0000; // Leftmost column visible in window // The options flags that comprise $grbit $fDspFmla = 0; // 0 - bit $fDspGrid = $this->phpSheet->getShowGridlines() ? 1 : 0; // 1 $fDspRwCol = $this->phpSheet->getShowRowColHeaders() ? 1 : 0; // 2 $fFrozen = $this->phpSheet->getFreezePane() ? 1 : 0; // 3 $fDspZeros = 1; // 4 $fDefaultHdr = 1; // 5 $fArabic = $this->phpSheet->getRightToLeft() ? 1 : 0; // 6 $fDspGuts = $this->outlineOn; // 7 $fFrozenNoSplit = 0; // 0 - bit // no support in PhpSpreadsheet for selected sheet, therefore sheet is only selected if it is the active sheet $fSelected = ($this->phpSheet === $this->phpSheet->getParent()->getActiveSheet()) ? 1 : 0; $fPageBreakPreview = $this->phpSheet->getSheetView()->getView() === SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW; $grbit = $fDspFmla; $grbit |= $fDspGrid << 1; $grbit |= $fDspRwCol << 2; $grbit |= $fFrozen << 3; $grbit |= $fDspZeros << 4; $grbit |= $fDefaultHdr << 5; $grbit |= $fArabic << 6; $grbit |= $fDspGuts << 7; $grbit |= $fFrozenNoSplit << 8; $grbit |= $fSelected << 9; // Selected sheets. $grbit |= $fSelected << 10; // Active sheet. $grbit |= $fPageBreakPreview << 11; $header = pack('vv', $record, $length); $data = pack('vvv', $grbit, $rwTop, $colLeft); // FIXME !!! $rgbHdr = 0x0040; // Row/column heading and gridline color index $zoom_factor_page_break = ($fPageBreakPreview ? $this->phpSheet->getSheetView()->getZoomScale() : 0x0000); $zoom_factor_normal = $this->phpSheet->getSheetView()->getZoomScaleNormal(); $data .= pack('vvvvV', $rgbHdr, 0x0000, $zoom_factor_page_break, $zoom_factor_normal, 0x00000000); $this->append($header . $data); } /** * Write BIFF record DEFAULTROWHEIGHT. */ private function writeDefaultRowHeight(): void { $defaultRowHeight = $this->phpSheet->getDefaultRowDimension()->getRowHeight(); if ($defaultRowHeight < 0) { return; } // convert to twips $defaultRowHeight = (int) 20 * $defaultRowHeight; $record = 0x0225; // Record identifier $length = 0x0004; // Number of bytes to follow $header = pack('vv', $record, $length); $data = pack('vv', 1, $defaultRowHeight); $this->append($header . $data); } /** * Write BIFF record DEFCOLWIDTH if COLINFO records are in use. */ private function writeDefcol(): void { $defaultColWidth = 8; $record = 0x0055; // Record identifier $length = 0x0002; // Number of bytes to follow $header = pack('vv', $record, $length); $data = pack('v', $defaultColWidth); $this->append($header . $data); } /** * Write BIFF record COLINFO to define column widths. * * Note: The SDK says the record length is 0x0B but Excel writes a 0x0C * length record. * * @param array $col_array This is the only parameter received and is composed of the following: * 0 => First formatted column, * 1 => Last formatted column, * 2 => Col width (8.43 is Excel default), * 3 => The optional XF format of the column, * 4 => Option flags. * 5 => Optional outline level */ private function writeColinfo($col_array): void { $colFirst = $col_array[0] ?? null; $colLast = $col_array[1] ?? null; $coldx = $col_array[2] ?? 8.43; $xfIndex = $col_array[3] ?? 15; $grbit = $col_array[4] ?? 0; $level = $col_array[5] ?? 0; $record = 0x007D; // Record identifier $length = 0x000C; // Number of bytes to follow $coldx *= 256; // Convert to units of 1/256 of a char $ixfe = $xfIndex; $reserved = 0x0000; // Reserved $level = max(0, min($level, 7)); $grbit |= $level << 8; $header = pack('vv', $record, $length); $data = pack('vvvvvv', $colFirst, $colLast, $coldx, $ixfe, $grbit, $reserved); $this->append($header . $data); } /** * Write BIFF record SELECTION. */ private function writeSelection(): void { // look up the selected cell range $selectedCells = Coordinate::splitRange($this->phpSheet->getSelectedCells()); $selectedCells = $selectedCells[0]; if (count($selectedCells) == 2) { [$first, $last] = $selectedCells; } else { $first = $selectedCells[0]; $last = $selectedCells[0]; } [$colFirst, $rwFirst] = Coordinate::coordinateFromString($first); $colFirst = Coordinate::columnIndexFromString($colFirst) - 1; // base 0 column index --$rwFirst; // base 0 row index [$colLast, $rwLast] = Coordinate::coordinateFromString($last); $colLast = Coordinate::columnIndexFromString($colLast) - 1; // base 0 column index --$rwLast; // base 0 row index // make sure we are not out of bounds $colFirst = min($colFirst, 255); $colLast = min($colLast, 255); $rwFirst = min($rwFirst, 65535); $rwLast = min($rwLast, 65535); $record = 0x001D; // Record identifier $length = 0x000F; // Number of bytes to follow $pnn = $this->activePane; // Pane position $rwAct = $rwFirst; // Active row $colAct = $colFirst; // Active column $irefAct = 0; // Active cell ref $cref = 1; // Number of refs // Swap last row/col for first row/col as necessary if ($rwFirst > $rwLast) { [$rwFirst, $rwLast] = [$rwLast, $rwFirst]; } if ($colFirst > $colLast) { [$colFirst, $colLast] = [$colLast, $colFirst]; } $header = pack('vv', $record, $length); $data = pack('CvvvvvvCC', $pnn, $rwAct, $colAct, $irefAct, $cref, $rwFirst, $rwLast, $colFirst, $colLast); $this->append($header . $data); } /** * Store the MERGEDCELLS records for all ranges of merged cells. */ private function writeMergedCells(): void { $mergeCells = $this->phpSheet->getMergeCells(); $countMergeCells = count($mergeCells); if ($countMergeCells == 0) { return; } // maximum allowed number of merged cells per record $maxCountMergeCellsPerRecord = 1027; // record identifier $record = 0x00E5; // counter for total number of merged cells treated so far by the writer $i = 0; // counter for number of merged cells written in record currently being written $j = 0; // initialize record data $recordData = ''; // loop through the merged cells foreach ($mergeCells as $mergeCell) { ++$i; ++$j; // extract the row and column indexes $range = Coordinate::splitRange($mergeCell); [$first, $last] = $range[0]; [$firstColumn, $firstRow] = Coordinate::indexesFromString($first); [$lastColumn, $lastRow] = Coordinate::indexesFromString($last); $recordData .= pack('vvvv', $firstRow - 1, $lastRow - 1, $firstColumn - 1, $lastColumn - 1); // flush record if we have reached limit for number of merged cells, or reached final merged cell if ($j == $maxCountMergeCellsPerRecord || $i == $countMergeCells) { $recordData = pack('v', $j) . $recordData; $length = strlen($recordData); $header = pack('vv', $record, $length); $this->append($header . $recordData); // initialize for next record, if any $recordData = ''; $j = 0; } } } /** * Write SHEETLAYOUT record. */ private function writeSheetLayout(): void { if (!$this->phpSheet->isTabColorSet()) { return; } $recordData = pack( 'vvVVVvv', 0x0862, 0x0000, // unused 0x00000000, // unused 0x00000000, // unused 0x00000014, // size of record data $this->colors[$this->phpSheet->getTabColor()->getRGB()], // color index 0x0000 // unused ); $length = strlen($recordData); $record = 0x0862; // Record identifier $header = pack('vv', $record, $length); $this->append($header . $recordData); } /** * Write SHEETPROTECTION. */ private function writeSheetProtection(): void { // record identifier $record = 0x0867; // prepare options $options = (int) !$this->phpSheet->getProtection()->getObjects() | (int) !$this->phpSheet->getProtection()->getScenarios() << 1 | (int) !$this->phpSheet->getProtection()->getFormatCells() << 2 | (int) !$this->phpSheet->getProtection()->getFormatColumns() << 3 | (int) !$this->phpSheet->getProtection()->getFormatRows() << 4 | (int) !$this->phpSheet->getProtection()->getInsertColumns() << 5 | (int) !$this->phpSheet->getProtection()->getInsertRows() << 6 | (int) !$this->phpSheet->getProtection()->getInsertHyperlinks() << 7 | (int) !$this->phpSheet->getProtection()->getDeleteColumns() << 8 | (int) !$this->phpSheet->getProtection()->getDeleteRows() << 9 | (int) !$this->phpSheet->getProtection()->getSelectLockedCells() << 10 | (int) !$this->phpSheet->getProtection()->getSort() << 11 | (int) !$this->phpSheet->getProtection()->getAutoFilter() << 12 | (int) !$this->phpSheet->getProtection()->getPivotTables() << 13 | (int) !$this->phpSheet->getProtection()->getSelectUnlockedCells() << 14; // record data $recordData = pack( 'vVVCVVvv', 0x0867, // repeated record identifier 0x0000, // not used 0x0000, // not used 0x00, // not used 0x01000200, // unknown data 0xFFFFFFFF, // unknown data $options, // options 0x0000 // not used ); $length = strlen($recordData); $header = pack('vv', $record, $length); $this->append($header . $recordData); } /** * Write BIFF record RANGEPROTECTION. * * Openoffice.org's Documentation of the Microsoft Excel File Format uses term RANGEPROTECTION for these records * Microsoft Office Excel 97-2007 Binary File Format Specification uses term FEAT for these records */ private function writeRangeProtection(): void { foreach ($this->phpSheet->getProtectedCells() as $range => $password) { // number of ranges, e.g. 'A1:B3 C20:D25' $cellRanges = explode(' ', $range); $cref = count($cellRanges); $recordData = pack( 'vvVVvCVvVv', 0x0868, 0x00, 0x0000, 0x0000, 0x02, 0x0, 0x0000, $cref, 0x0000, 0x00 ); foreach ($cellRanges as $cellRange) { $recordData .= $this->writeBIFF8CellRangeAddressFixed($cellRange); } // the rgbFeat structure $recordData .= pack( 'VV', 0x0000, hexdec($password) ); $recordData .= StringHelper::UTF8toBIFF8UnicodeLong('p' . md5($recordData)); $length = strlen($recordData); $record = 0x0868; // Record identifier $header = pack('vv', $record, $length); $this->append($header . $recordData); } } /** * Writes the Excel BIFF PANE record. * The panes can either be frozen or thawed (unfrozen). * Frozen panes are specified in terms of an integer number of rows and columns. * Thawed panes are specified in terms of Excel's units for rows and columns. */ private function writePanes(): void { if (!$this->phpSheet->getFreezePane()) { // thaw panes return; } [$column, $row] = Coordinate::indexesFromString($this->phpSheet->getFreezePane() ?? ''); $x = $column - 1; $y = $row - 1; [$leftMostColumn, $topRow] = Coordinate::indexesFromString($this->phpSheet->getTopLeftCell()); //Coordinates are zero-based in xls files $rwTop = $topRow - 1; $colLeft = $leftMostColumn - 1; $record = 0x0041; // Record identifier $length = 0x000A; // Number of bytes to follow // Determine which pane should be active. There is also the undocumented // option to override this should it be necessary: may be removed later. $pnnAct = null; if ($x != 0 && $y != 0) { $pnnAct = 0; // Bottom right } if ($x != 0 && $y == 0) { $pnnAct = 1; // Top right } if ($x == 0 && $y != 0) { $pnnAct = 2; // Bottom left } if ($x == 0 && $y == 0) { $pnnAct = 3; // Top left } $this->activePane = $pnnAct; // Used in writeSelection $header = pack('vv', $record, $length); $data = pack('vvvvv', $x, $y, $rwTop, $colLeft, $pnnAct); $this->append($header . $data); } /** * Store the page setup SETUP BIFF record. */ private function writeSetup(): void { $record = 0x00A1; // Record identifier $length = 0x0022; // Number of bytes to follow $iPaperSize = $this->phpSheet->getPageSetup()->getPaperSize(); // Paper size $iScale = $this->phpSheet->getPageSetup()->getScale() ?: 100; // Print scaling factor $iPageStart = 0x01; // Starting page number $iFitWidth = (int) $this->phpSheet->getPageSetup()->getFitToWidth(); // Fit to number of pages wide $iFitHeight = (int) $this->phpSheet->getPageSetup()->getFitToHeight(); // Fit to number of pages high $grbit = 0x00; // Option flags $iRes = 0x0258; // Print resolution $iVRes = 0x0258; // Vertical print resolution $numHdr = $this->phpSheet->getPageMargins()->getHeader(); // Header Margin $numFtr = $this->phpSheet->getPageMargins()->getFooter(); // Footer Margin $iCopies = 0x01; // Number of copies // Order of printing pages $fLeftToRight = $this->phpSheet->getPageSetup()->getPageOrder() === PageSetup::PAGEORDER_DOWN_THEN_OVER ? 0x1 : 0x0; // Page orientation $fLandscape = ($this->phpSheet->getPageSetup()->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE) ? 0x0 : 0x1; $fNoPls = 0x0; // Setup not read from printer $fNoColor = 0x0; // Print black and white $fDraft = 0x0; // Print draft quality $fNotes = 0x0; // Print notes $fNoOrient = 0x0; // Orientation not set $fUsePage = 0x0; // Use custom starting page $grbit = $fLeftToRight; $grbit |= $fLandscape << 1; $grbit |= $fNoPls << 2; $grbit |= $fNoColor << 3; $grbit |= $fDraft << 4; $grbit |= $fNotes << 5; $grbit |= $fNoOrient << 6; $grbit |= $fUsePage << 7; $numHdr = pack('d', $numHdr); $numFtr = pack('d', $numFtr); if (self::getByteOrder()) { // if it's Big Endian $numHdr = strrev($numHdr); $numFtr = strrev($numFtr); } $header = pack('vv', $record, $length); $data1 = pack('vvvvvvvv', $iPaperSize, $iScale, $iPageStart, $iFitWidth, $iFitHeight, $grbit, $iRes, $iVRes); $data2 = $numHdr . $numFtr; $data3 = pack('v', $iCopies); $this->append($header . $data1 . $data2 . $data3); } /** * Store the header caption BIFF record. */ private function writeHeader(): void { $record = 0x0014; // Record identifier /* removing for now // need to fix character count (multibyte!) if (strlen($this->phpSheet->getHeaderFooter()->getOddHeader()) <= 255) { $str = $this->phpSheet->getHeaderFooter()->getOddHeader(); // header string } else { $str = ''; } */ $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddHeader()); $length = strlen($recordData); $header = pack('vv', $record, $length); $this->append($header . $recordData); } /** * Store the footer caption BIFF record. */ private function writeFooter(): void { $record = 0x0015; // Record identifier /* removing for now // need to fix character count (multibyte!) if (strlen($this->phpSheet->getHeaderFooter()->getOddFooter()) <= 255) { $str = $this->phpSheet->getHeaderFooter()->getOddFooter(); } else { $str = ''; } */ $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddFooter()); $length = strlen($recordData); $header = pack('vv', $record, $length); $this->append($header . $recordData); } /** * Store the horizontal centering HCENTER BIFF record. */ private function writeHcenter(): void { $record = 0x0083; // Record identifier $length = 0x0002; // Bytes to follow $fHCenter = $this->phpSheet->getPageSetup()->getHorizontalCentered() ? 1 : 0; // Horizontal centering $header = pack('vv', $record, $length); $data = pack('v', $fHCenter); $this->append($header . $data); } /** * Store the vertical centering VCENTER BIFF record. */ private function writeVcenter(): void { $record = 0x0084; // Record identifier $length = 0x0002; // Bytes to follow $fVCenter = $this->phpSheet->getPageSetup()->getVerticalCentered() ? 1 : 0; // Horizontal centering $header = pack('vv', $record, $length); $data = pack('v', $fVCenter); $this->append($header . $data); } /** * Store the LEFTMARGIN BIFF record. */ private function writeMarginLeft(): void { $record = 0x0026; // Record identifier $length = 0x0008; // Bytes to follow $margin = $this->phpSheet->getPageMargins()->getLeft(); // Margin in inches $header = pack('vv', $record, $length); $data = pack('d', $margin); if (self::getByteOrder()) { // if it's Big Endian $data = strrev($data); } $this->append($header . $data); } /** * Store the RIGHTMARGIN BIFF record. */ private function writeMarginRight(): void { $record = 0x0027; // Record identifier $length = 0x0008; // Bytes to follow $margin = $this->phpSheet->getPageMargins()->getRight(); // Margin in inches $header = pack('vv', $record, $length); $data = pack('d', $margin); if (self::getByteOrder()) { // if it's Big Endian $data = strrev($data); } $this->append($header . $data); } /** * Store the TOPMARGIN BIFF record. */ private function writeMarginTop(): void { $record = 0x0028; // Record identifier $length = 0x0008; // Bytes to follow $margin = $this->phpSheet->getPageMargins()->getTop(); // Margin in inches $header = pack('vv', $record, $length); $data = pack('d', $margin); if (self::getByteOrder()) { // if it's Big Endian $data = strrev($data); } $this->append($header . $data); } /** * Store the BOTTOMMARGIN BIFF record. */ private function writeMarginBottom(): void { $record = 0x0029; // Record identifier $length = 0x0008; // Bytes to follow $margin = $this->phpSheet->getPageMargins()->getBottom(); // Margin in inches $header = pack('vv', $record, $length); $data = pack('d', $margin); if (self::getByteOrder()) { // if it's Big Endian $data = strrev($data); } $this->append($header . $data); } /** * Write the PRINTHEADERS BIFF record. */ private function writePrintHeaders(): void { $record = 0x002a; // Record identifier $length = 0x0002; // Bytes to follow $fPrintRwCol = $this->printHeaders; // Boolean flag $header = pack('vv', $record, $length); $data = pack('v', $fPrintRwCol); $this->append($header . $data); } /** * Write the PRINTGRIDLINES BIFF record. Must be used in conjunction with the * GRIDSET record. */ private function writePrintGridlines(): void { $record = 0x002b; // Record identifier $length = 0x0002; // Bytes to follow $fPrintGrid = $this->phpSheet->getPrintGridlines() ? 1 : 0; // Boolean flag $header = pack('vv', $record, $length); $data = pack('v', $fPrintGrid); $this->append($header . $data); } /** * Write the GRIDSET BIFF record. Must be used in conjunction with the * PRINTGRIDLINES record. */ private function writeGridset(): void { $record = 0x0082; // Record identifier $length = 0x0002; // Bytes to follow $fGridSet = !$this->phpSheet->getPrintGridlines(); // Boolean flag $header = pack('vv', $record, $length); $data = pack('v', $fGridSet); $this->append($header . $data); } /** * Write the AUTOFILTERINFO BIFF record. This is used to configure the number of autofilter select used in the sheet. */ private function writeAutoFilterInfo(): void { $record = 0x009D; // Record identifier $length = 0x0002; // Bytes to follow $rangeBounds = Coordinate::rangeBoundaries($this->phpSheet->getAutoFilter()->getRange()); $iNumFilters = 1 + $rangeBounds[1][0] - $rangeBounds[0][0]; $header = pack('vv', $record, $length); $data = pack('v', $iNumFilters); $this->append($header . $data); } /** * Write the GUTS BIFF record. This is used to configure the gutter margins * where Excel outline symbols are displayed. The visibility of the gutters is * controlled by a flag in WSBOOL. * * @see writeWsbool() */ private function writeGuts(): void { $record = 0x0080; // Record identifier $length = 0x0008; // Bytes to follow $dxRwGut = 0x0000; // Size of row gutter $dxColGut = 0x0000; // Size of col gutter // determine maximum row outline level $maxRowOutlineLevel = 0; foreach ($this->phpSheet->getRowDimensions() as $rowDimension) { $maxRowOutlineLevel = max($maxRowOutlineLevel, $rowDimension->getOutlineLevel()); } $col_level = 0; // Calculate the maximum column outline level. The equivalent calculation // for the row outline level is carried out in writeRow(). $colcount = count($this->columnInfo); for ($i = 0; $i < $colcount; ++$i) { $col_level = max($this->columnInfo[$i][5], $col_level); } // Set the limits for the outline levels (0 <= x <= 7). $col_level = max(0, min($col_level, 7)); // The displayed level is one greater than the max outline levels if ($maxRowOutlineLevel) { ++$maxRowOutlineLevel; } if ($col_level) { ++$col_level; } $header = pack('vv', $record, $length); $data = pack('vvvv', $dxRwGut, $dxColGut, $maxRowOutlineLevel, $col_level); $this->append($header . $data); } /** * Write the WSBOOL BIFF record, mainly for fit-to-page. Used in conjunction * with the SETUP record. */ private function writeWsbool(): void { $record = 0x0081; // Record identifier $length = 0x0002; // Bytes to follow $grbit = 0x0000; // The only option that is of interest is the flag for fit to page. So we // set all the options in one go. // // Set the option flags $grbit |= 0x0001; // Auto page breaks visible if ($this->outlineStyle) { $grbit |= 0x0020; // Auto outline styles } if ($this->phpSheet->getShowSummaryBelow()) { $grbit |= 0x0040; // Outline summary below } if ($this->phpSheet->getShowSummaryRight()) { $grbit |= 0x0080; // Outline summary right } if ($this->phpSheet->getPageSetup()->getFitToPage()) { $grbit |= 0x0100; // Page setup fit to page } if ($this->outlineOn) { $grbit |= 0x0400; // Outline symbols displayed } $header = pack('vv', $record, $length); $data = pack('v', $grbit); $this->append($header . $data); } /** * Write the HORIZONTALPAGEBREAKS and VERTICALPAGEBREAKS BIFF records. */ private function writeBreaks(): void { // initialize $vbreaks = []; $hbreaks = []; foreach ($this->phpSheet->getBreaks() as $cell => $breakType) { // Fetch coordinates $coordinates = Coordinate::coordinateFromString($cell); // Decide what to do by the type of break switch ($breakType) { case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_COLUMN: // Add to list of vertical breaks $vbreaks[] = Coordinate::columnIndexFromString($coordinates[0]) - 1; break; case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_ROW: // Add to list of horizontal breaks $hbreaks[] = $coordinates[1]; break; case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_NONE: default: // Nothing to do break; } } //horizontal page breaks if (!empty($hbreaks)) { // Sort and filter array of page breaks sort($hbreaks, SORT_NUMERIC); if ($hbreaks[0] == 0) { // don't use first break if it's 0 array_shift($hbreaks); } $record = 0x001b; // Record identifier $cbrk = count($hbreaks); // Number of page breaks $length = 2 + 6 * $cbrk; // Bytes to follow $header = pack('vv', $record, $length); $data = pack('v', $cbrk); // Append each page break foreach ($hbreaks as $hbreak) { $data .= pack('vvv', $hbreak, 0x0000, 0x00ff); } $this->append($header . $data); } // vertical page breaks if (!empty($vbreaks)) { // 1000 vertical pagebreaks appears to be an internal Excel 5 limit. // It is slightly higher in Excel 97/200, approx. 1026 $vbreaks = array_slice($vbreaks, 0, 1000); // Sort and filter array of page breaks sort($vbreaks, SORT_NUMERIC); if ($vbreaks[0] == 0) { // don't use first break if it's 0 array_shift($vbreaks); } $record = 0x001a; // Record identifier $cbrk = count($vbreaks); // Number of page breaks $length = 2 + 6 * $cbrk; // Bytes to follow $header = pack('vv', $record, $length); $data = pack('v', $cbrk); // Append each page break foreach ($vbreaks as $vbreak) { $data .= pack('vvv', $vbreak, 0x0000, 0xffff); } $this->append($header . $data); } } /** * Set the Biff PROTECT record to indicate that the worksheet is protected. */ private function writeProtect(): void { // Exit unless sheet protection has been specified if (!$this->phpSheet->getProtection()->getSheet()) { return; } $record = 0x0012; // Record identifier $length = 0x0002; // Bytes to follow $fLock = 1; // Worksheet is protected $header = pack('vv', $record, $length); $data = pack('v', $fLock); $this->append($header . $data); } /** * Write SCENPROTECT. */ private function writeScenProtect(): void { // Exit if sheet protection is not active if (!$this->phpSheet->getProtection()->getSheet()) { return; } // Exit if scenarios are not protected if (!$this->phpSheet->getProtection()->getScenarios()) { return; } $record = 0x00DD; // Record identifier $length = 0x0002; // Bytes to follow $header = pack('vv', $record, $length); $data = pack('v', 1); $this->append($header . $data); } /** * Write OBJECTPROTECT. */ private function writeObjectProtect(): void { // Exit if sheet protection is not active if (!$this->phpSheet->getProtection()->getSheet()) { return; } // Exit if objects are not protected if (!$this->phpSheet->getProtection()->getObjects()) { return; } $record = 0x0063; // Record identifier $length = 0x0002; // Bytes to follow $header = pack('vv', $record, $length); $data = pack('v', 1); $this->append($header . $data); } /** * Write the worksheet PASSWORD record. */ private function writePassword(): void { // Exit unless sheet protection and password have been specified if (!$this->phpSheet->getProtection()->getSheet() || !$this->phpSheet->getProtection()->getPassword() || $this->phpSheet->getProtection()->getAlgorithm() !== '') { return; } $record = 0x0013; // Record identifier $length = 0x0002; // Bytes to follow $wPassword = hexdec($this->phpSheet->getProtection()->getPassword()); // Encoded password $header = pack('vv', $record, $length); $data = pack('v', $wPassword); $this->append($header . $data); } /** * Insert a 24bit bitmap image in a worksheet. * * @param int $row The row we are going to insert the bitmap into * @param int $col The column we are going to insert the bitmap into * @param mixed $bitmap The bitmap filename or GD-image resource * @param int $x the horizontal position (offset) of the image inside the cell * @param int $y the vertical position (offset) of the image inside the cell * @param float $scale_x The horizontal scale * @param float $scale_y The vertical scale */ public function insertBitmap($row, $col, $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1): void { $bitmap_array = (is_resource($bitmap) || $bitmap instanceof GdImage ? $this->processBitmapGd($bitmap) : $this->processBitmap($bitmap)); [$width, $height, $size, $data] = $bitmap_array; // Scale the frame of the image. $width *= $scale_x; $height *= $scale_y; // Calculate the vertices of the image and write the OBJ record $this->positionImage($col, $row, $x, $y, $width, $height); // Write the IMDATA record to store the bitmap data $record = 0x007f; $length = 8 + $size; $cf = 0x09; $env = 0x01; $lcb = $size; $header = pack('vvvvV', $record, $length, $cf, $env, $lcb); $this->append($header . $data); } /** * Calculate the vertices that define the position of the image as required by * the OBJ record. * * +------------+------------+ * | A | B | * +-----+------------+------------+ * | |(x1,y1) | | * | 1 |(A1)._______|______ | * | | | | | * | | | | | * +-----+----| BITMAP |-----+ * | | | | | * | 2 | |______________. | * | | | (B2)| * | | | (x2,y2)| * +---- +------------+------------+ * * Example of a bitmap that covers some of the area from cell A1 to cell B2. * * Based on the width and height of the bitmap we need to calculate 8 vars: * $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2. * The width and height of the cells are also variable and have to be taken into * account. * The values of $col_start and $row_start are passed in from the calling * function. The values of $col_end and $row_end are calculated by subtracting * the width and height of the bitmap from the width and height of the * underlying cells. * The vertices are expressed as a percentage of the underlying cell width as * follows (rhs values are in pixels): * * x1 = X / W *1024 * y1 = Y / H *256 * x2 = (X-1) / W *1024 * y2 = (Y-1) / H *256 * * Where: X is distance from the left side of the underlying cell * Y is distance from the top of the underlying cell * W is the width of the cell * H is the height of the cell * The SDK incorrectly states that the height should be expressed as a * percentage of 1024. * * @param int $col_start Col containing upper left corner of object * @param int $row_start Row containing top left corner of object * @param int $x1 Distance to left side of object * @param int $y1 Distance to top of object * @param int $width Width of image frame * @param int $height Height of image frame */ public function positionImage($col_start, $row_start, $x1, $y1, $width, $height): void { // Initialise end cell to the same as the start cell $col_end = $col_start; // Col containing lower right corner of object $row_end = $row_start; // Row containing bottom right corner of object // Zero the specified offset if greater than the cell dimensions if ($x1 >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1))) { $x1 = 0; } if ($y1 >= Xls::sizeRow($this->phpSheet, $row_start + 1)) { $y1 = 0; } $width = $width + $x1 - 1; $height = $height + $y1 - 1; // Subtract the underlying cell widths to find the end cell of the image while ($width >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1))) { $width -= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)); ++$col_end; } // Subtract the underlying cell heights to find the end cell of the image while ($height >= Xls::sizeRow($this->phpSheet, $row_end + 1)) { $height -= Xls::sizeRow($this->phpSheet, $row_end + 1); ++$row_end; } // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell // with zero eight or width. // if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) == 0) { return; } if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) == 0) { return; } if (Xls::sizeRow($this->phpSheet, $row_start + 1) == 0) { return; } if (Xls::sizeRow($this->phpSheet, $row_end + 1) == 0) { return; } // Convert the pixel values to the percentage value expected by Excel $x1 = $x1 / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) * 1024; $y1 = $y1 / Xls::sizeRow($this->phpSheet, $row_start + 1) * 256; $x2 = $width / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) * 1024; // Distance to right side of object $y2 = $height / Xls::sizeRow($this->phpSheet, $row_end + 1) * 256; // Distance to bottom of object $this->writeObjPicture($col_start, $x1, $row_start, $y1, $col_end, $x2, $row_end, $y2); } /** * Store the OBJ record that precedes an IMDATA record. This could be generalise * to support other Excel objects. * * @param int $colL Column containing upper left corner of object * @param int $dxL Distance from left side of cell * @param int $rwT Row containing top left corner of object * @param int $dyT Distance from top of cell * @param int $colR Column containing lower right corner of object * @param int $dxR Distance from right of cell * @param int $rwB Row containing bottom right corner of object * @param int $dyB Distance from bottom of cell */ private function writeObjPicture($colL, $dxL, $rwT, $dyT, $colR, $dxR, $rwB, $dyB): void { $record = 0x005d; // Record identifier $length = 0x003c; // Bytes to follow $cObj = 0x0001; // Count of objects in file (set to 1) $OT = 0x0008; // Object type. 8 = Picture $id = 0x0001; // Object ID $grbit = 0x0614; // Option flags $cbMacro = 0x0000; // Length of FMLA structure $Reserved1 = 0x0000; // Reserved $Reserved2 = 0x0000; // Reserved $icvBack = 0x09; // Background colour $icvFore = 0x09; // Foreground colour $fls = 0x00; // Fill pattern $fAuto = 0x00; // Automatic fill $icv = 0x08; // Line colour $lns = 0xff; // Line style $lnw = 0x01; // Line weight $fAutoB = 0x00; // Automatic border $frs = 0x0000; // Frame style $cf = 0x0009; // Image format, 9 = bitmap $Reserved3 = 0x0000; // Reserved $cbPictFmla = 0x0000; // Length of FMLA structure $Reserved4 = 0x0000; // Reserved $grbit2 = 0x0001; // Option flags $Reserved5 = 0x0000; // Reserved $header = pack('vv', $record, $length); $data = pack('V', $cObj); $data .= pack('v', $OT); $data .= pack('v', $id); $data .= pack('v', $grbit); $data .= pack('v', $colL); $data .= pack('v', $dxL); $data .= pack('v', $rwT); $data .= pack('v', $dyT); $data .= pack('v', $colR); $data .= pack('v', $dxR); $data .= pack('v', $rwB); $data .= pack('v', $dyB); $data .= pack('v', $cbMacro); $data .= pack('V', $Reserved1); $data .= pack('v', $Reserved2); $data .= pack('C', $icvBack); $data .= pack('C', $icvFore); $data .= pack('C', $fls); $data .= pack('C', $fAuto); $data .= pack('C', $icv); $data .= pack('C', $lns); $data .= pack('C', $lnw); $data .= pack('C', $fAutoB); $data .= pack('v', $frs); $data .= pack('V', $cf); $data .= pack('v', $Reserved3); $data .= pack('v', $cbPictFmla); $data .= pack('v', $Reserved4); $data .= pack('v', $grbit2); $data .= pack('V', $Reserved5); $this->append($header . $data); } /** * Convert a GD-image into the internal format. * * @param GdImage|resource $image The image to process * * @return array Array with data and properties of the bitmap */ public function processBitmapGd($image) { $width = imagesx($image); $height = imagesy($image); $data = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18); for ($j = $height; --$j;) { for ($i = 0; $i < $width; ++$i) { $color = imagecolorsforindex($image, imagecolorat($image, $i, $j)); foreach (['red', 'green', 'blue'] as $key) { $color[$key] = $color[$key] + round((255 - $color[$key]) * $color['alpha'] / 127); } $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']); } if (3 * $width % 4) { $data .= str_repeat("\x00", 4 - 3 * $width % 4); } } return [$width, $height, strlen($data), $data]; } /** * Convert a 24 bit bitmap into the modified internal format used by Windows. * This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the * MSDN library. * * @param string $bitmap The bitmap to process * * @return array Array with data and properties of the bitmap */ public function processBitmap($bitmap) { // Open file. $bmp_fd = @fopen($bitmap, 'rb'); if (!$bmp_fd) { throw new WriterException("Couldn't import $bitmap"); } // Slurp the file into a string. $data = fread($bmp_fd, filesize($bitmap)); // Check that the file is big enough to be a bitmap. if (strlen($data) <= 0x36) { throw new WriterException("$bitmap doesn't contain enough data.\n"); } // The first 2 bytes are used to identify the bitmap. $identity = unpack('A2ident', $data); if ($identity['ident'] != 'BM') { throw new WriterException("$bitmap doesn't appear to be a valid bitmap image.\n"); } // Remove bitmap data: ID. $data = substr($data, 2); // Read and remove the bitmap size. This is more reliable than reading // the data size at offset 0x22. // $size_array = unpack('Vsa', substr($data, 0, 4)); $size = $size_array['sa']; $data = substr($data, 4); $size -= 0x36; // Subtract size of bitmap header. $size += 0x0C; // Add size of BIFF header. // Remove bitmap data: reserved, offset, header length. $data = substr($data, 12); // Read and remove the bitmap width and height. Verify the sizes. $width_and_height = unpack('V2', substr($data, 0, 8)); $width = $width_and_height[1]; $height = $width_and_height[2]; $data = substr($data, 8); if ($width > 0xFFFF) { throw new WriterException("$bitmap: largest image width supported is 65k.\n"); } if ($height > 0xFFFF) { throw new WriterException("$bitmap: largest image height supported is 65k.\n"); } // Read and remove the bitmap planes and bpp data. Verify them. $planes_and_bitcount = unpack('v2', substr($data, 0, 4)); $data = substr($data, 4); if ($planes_and_bitcount[2] != 24) { // Bitcount throw new WriterException("$bitmap isn't a 24bit true color bitmap.\n"); } if ($planes_and_bitcount[1] != 1) { throw new WriterException("$bitmap: only 1 plane supported in bitmap image.\n"); } // Read and remove the bitmap compression. Verify compression. $compression = unpack('Vcomp', substr($data, 0, 4)); $data = substr($data, 4); if ($compression['comp'] != 0) { throw new WriterException("$bitmap: compression not supported in bitmap image.\n"); } // Remove bitmap data: data size, hres, vres, colours, imp. colours. $data = substr($data, 20); // Add the BITMAPCOREHEADER data $header = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18); $data = $header . $data; return [$width, $height, $size, $data]; } /** * Store the window zoom factor. This should be a reduced fraction but for * simplicity we will store all fractions with a numerator of 100. */ private function writeZoom(): void { // If scale is 100 we don't need to write a record if ($this->phpSheet->getSheetView()->getZoomScale() == 100) { return; } $record = 0x00A0; // Record identifier $length = 0x0004; // Bytes to follow $header = pack('vv', $record, $length); $data = pack('vv', $this->phpSheet->getSheetView()->getZoomScale(), 100); $this->append($header . $data); } /** * Get Escher object. */ public function getEscher(): ?\PhpOffice\PhpSpreadsheet\Shared\Escher { return $this->escher; } /** * Set Escher object. */ public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher): void { $this->escher = $escher; } /** * Write MSODRAWING record. */ private function writeMsoDrawing(): void { // write the Escher stream if necessary if (isset($this->escher)) { $writer = new Escher($this->escher); $data = $writer->close(); $spOffsets = $writer->getSpOffsets(); $spTypes = $writer->getSpTypes(); // write the neccesary MSODRAWING, OBJ records // split the Escher stream $spOffsets[0] = 0; $nm = count($spOffsets) - 1; // number of shapes excluding first shape for ($i = 1; $i <= $nm; ++$i) { // MSODRAWING record $record = 0x00EC; // Record identifier // chunk of Escher stream for one shape $dataChunk = substr($data, $spOffsets[$i - 1], $spOffsets[$i] - $spOffsets[$i - 1]); $length = strlen($dataChunk); $header = pack('vv', $record, $length); $this->append($header . $dataChunk); // OBJ record $record = 0x005D; // record identifier $objData = ''; // ftCmo if ($spTypes[$i] == 0x00C9) { // Add ftCmo (common object data) subobject $objData .= pack( 'vvvvvVVV', 0x0015, // 0x0015 = ftCmo 0x0012, // length of ftCmo data 0x0014, // object type, 0x0014 = filter $i, // object id number, Excel seems to use 1-based index, local for the sheet 0x2101, // option flags, 0x2001 is what OpenOffice.org uses 0, // reserved 0, // reserved 0 // reserved ); // Add ftSbs Scroll bar subobject $objData .= pack('vv', 0x00C, 0x0014); $objData .= pack('H*', '0000000000000000640001000A00000010000100'); // Add ftLbsData (List box data) subobject $objData .= pack('vv', 0x0013, 0x1FEE); $objData .= pack('H*', '00000000010001030000020008005700'); } else { // Add ftCmo (common object data) subobject $objData .= pack( 'vvvvvVVV', 0x0015, // 0x0015 = ftCmo 0x0012, // length of ftCmo data 0x0008, // object type, 0x0008 = picture $i, // object id number, Excel seems to use 1-based index, local for the sheet 0x6011, // option flags, 0x6011 is what OpenOffice.org uses 0, // reserved 0, // reserved 0 // reserved ); } // ftEnd $objData .= pack( 'vv', 0x0000, // 0x0000 = ftEnd 0x0000 // length of ftEnd data ); $length = strlen($objData); $header = pack('vv', $record, $length); $this->append($header . $objData); } } } /** * Store the DATAVALIDATIONS and DATAVALIDATION records. */ private function writeDataValidity(): void { // Datavalidation collection $dataValidationCollection = $this->phpSheet->getDataValidationCollection(); // Write data validations? if (!empty($dataValidationCollection)) { // DATAVALIDATIONS record $record = 0x01B2; // Record identifier $length = 0x0012; // Bytes to follow $grbit = 0x0000; // Prompt box at cell, no cached validity data at DV records $horPos = 0x00000000; // Horizontal position of prompt box, if fixed position $verPos = 0x00000000; // Vertical position of prompt box, if fixed position $objId = 0xFFFFFFFF; // Object identifier of drop down arrow object, or -1 if not visible $header = pack('vv', $record, $length); $data = pack('vVVVV', $grbit, $horPos, $verPos, $objId, count($dataValidationCollection)); $this->append($header . $data); // DATAVALIDATION records $record = 0x01BE; // Record identifier foreach ($dataValidationCollection as $cellCoordinate => $dataValidation) { // options $options = 0x00000000; // data type $type = CellDataValidation::type($dataValidation); $options |= $type << 0; // error style $errorStyle = CellDataValidation::errorStyle($dataValidation); $options |= $errorStyle << 4; // explicit formula? if ($type == 0x03 && preg_match('/^\".*\"$/', $dataValidation->getFormula1())) { $options |= 0x01 << 7; } // empty cells allowed $options |= $dataValidation->getAllowBlank() << 8; // show drop down $options |= (!$dataValidation->getShowDropDown()) << 9; // show input message $options |= $dataValidation->getShowInputMessage() << 18; // show error message $options |= $dataValidation->getShowErrorMessage() << 19; // condition operator $operator = CellDataValidation::operator($dataValidation); $options |= $operator << 20; $data = pack('V', $options); // prompt title $promptTitle = $dataValidation->getPromptTitle() !== '' ? $dataValidation->getPromptTitle() : chr(0); $data .= StringHelper::UTF8toBIFF8UnicodeLong($promptTitle); // error title $errorTitle = $dataValidation->getErrorTitle() !== '' ? $dataValidation->getErrorTitle() : chr(0); $data .= StringHelper::UTF8toBIFF8UnicodeLong($errorTitle); // prompt text $prompt = $dataValidation->getPrompt() !== '' ? $dataValidation->getPrompt() : chr(0); $data .= StringHelper::UTF8toBIFF8UnicodeLong($prompt); // error text $error = $dataValidation->getError() !== '' ? $dataValidation->getError() : chr(0); $data .= StringHelper::UTF8toBIFF8UnicodeLong($error); // formula 1 try { $formula1 = $dataValidation->getFormula1(); if ($type == 0x03) { // list type $formula1 = str_replace(',', chr(0), $formula1); } $this->parser->parse($formula1); $formula1 = $this->parser->toReversePolish(); $sz1 = strlen($formula1); } catch (PhpSpreadsheetException $e) { $sz1 = 0; $formula1 = ''; } $data .= pack('vv', $sz1, 0x0000); $data .= $formula1; // formula 2 try { $formula2 = $dataValidation->getFormula2(); if ($formula2 === '') { throw new WriterException('No formula2'); } $this->parser->parse($formula2); $formula2 = $this->parser->toReversePolish(); $sz2 = strlen($formula2); } catch (PhpSpreadsheetException $e) { $sz2 = 0; $formula2 = ''; } $data .= pack('vv', $sz2, 0x0000); $data .= $formula2; // cell range address list $data .= pack('v', 0x0001); $data .= $this->writeBIFF8CellRangeAddressFixed($cellCoordinate); $length = strlen($data); $header = pack('vv', $record, $length); $this->append($header . $data); } } } /** * Write PLV Record. */ private function writePageLayoutView(): void { $record = 0x088B; // Record identifier $length = 0x0010; // Bytes to follow $rt = 0x088B; // 2 $grbitFrt = 0x0000; // 2 $reserved = 0x0000000000000000; // 8 $wScalvePLV = $this->phpSheet->getSheetView()->getZoomScale(); // 2 // The options flags that comprise $grbit if ($this->phpSheet->getSheetView()->getView() == SheetView::SHEETVIEW_PAGE_LAYOUT) { $fPageLayoutView = 1; } else { $fPageLayoutView = 0; } $fRulerVisible = 0; $fWhitespaceHidden = 0; $grbit = $fPageLayoutView; // 2 $grbit |= $fRulerVisible << 1; $grbit |= $fWhitespaceHidden << 3; $header = pack('vv', $record, $length); $data = pack('vvVVvv', $rt, $grbitFrt, 0x00000000, 0x00000000, $wScalvePLV, $grbit); $this->append($header . $data); } /** * Write CFRule Record. */ private function writeCFRule(Conditional $conditional): void { $record = 0x01B1; // Record identifier $type = null; // Type of the CF $operatorType = null; // Comparison operator if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) { $type = 0x02; $operatorType = 0x00; } elseif ($conditional->getConditionType() == Conditional::CONDITION_CELLIS) { $type = 0x01; switch ($conditional->getOperatorType()) { case Conditional::OPERATOR_NONE: $operatorType = 0x00; break; case Conditional::OPERATOR_EQUAL: $operatorType = 0x03; break; case Conditional::OPERATOR_GREATERTHAN: $operatorType = 0x05; break; case Conditional::OPERATOR_GREATERTHANOREQUAL: $operatorType = 0x07; break; case Conditional::OPERATOR_LESSTHAN: $operatorType = 0x06; break; case Conditional::OPERATOR_LESSTHANOREQUAL: $operatorType = 0x08; break; case Conditional::OPERATOR_NOTEQUAL: $operatorType = 0x04; break; case Conditional::OPERATOR_BETWEEN: $operatorType = 0x01; break; // not OPERATOR_NOTBETWEEN 0x02 } } // $szValue1 : size of the formula data for first value or formula // $szValue2 : size of the formula data for second value or formula $arrConditions = $conditional->getConditions(); $numConditions = count($arrConditions); if ($numConditions == 1) { $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); $szValue2 = 0x0000; $operand1 = pack('Cv', 0x1E, $arrConditions[0]); $operand2 = null; } elseif ($numConditions == 2 && ($conditional->getOperatorType() == Conditional::OPERATOR_BETWEEN)) { $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); $szValue2 = ($arrConditions[1] <= 65535 ? 3 : 0x0000); $operand1 = pack('Cv', 0x1E, $arrConditions[0]); $operand2 = pack('Cv', 0x1E, $arrConditions[1]); } else { $szValue1 = 0x0000; $szValue2 = 0x0000; $operand1 = null; $operand2 = null; } // $flags : Option flags // Alignment $bAlignHz = ($conditional->getStyle()->getAlignment()->getHorizontal() === null ? 1 : 0); $bAlignVt = ($conditional->getStyle()->getAlignment()->getVertical() === null ? 1 : 0); $bAlignWrapTx = ($conditional->getStyle()->getAlignment()->getWrapText() === false ? 1 : 0); $bTxRotation = ($conditional->getStyle()->getAlignment()->getTextRotation() === null ? 1 : 0); $bIndent = ($conditional->getStyle()->getAlignment()->getIndent() === 0 ? 1 : 0); $bShrinkToFit = ($conditional->getStyle()->getAlignment()->getShrinkToFit() === false ? 1 : 0); if ($bAlignHz == 0 || $bAlignVt == 0 || $bAlignWrapTx == 0 || $bTxRotation == 0 || $bIndent == 0 || $bShrinkToFit == 0) { $bFormatAlign = 1; } else { $bFormatAlign = 0; } // Protection $bProtLocked = ($conditional->getStyle()->getProtection()->getLocked() == null ? 1 : 0); $bProtHidden = ($conditional->getStyle()->getProtection()->getHidden() == null ? 1 : 0); if ($bProtLocked == 0 || $bProtHidden == 0) { $bFormatProt = 1; } else { $bFormatProt = 0; } // Border $bBorderLeft = ($conditional->getStyle()->getBorders()->getLeft()->getColor()->getARGB() == Color::COLOR_BLACK && $conditional->getStyle()->getBorders()->getLeft()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0); $bBorderRight = ($conditional->getStyle()->getBorders()->getRight()->getColor()->getARGB() == Color::COLOR_BLACK && $conditional->getStyle()->getBorders()->getRight()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0); $bBorderTop = ($conditional->getStyle()->getBorders()->getTop()->getColor()->getARGB() == Color::COLOR_BLACK && $conditional->getStyle()->getBorders()->getTop()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0); $bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getColor()->getARGB() == Color::COLOR_BLACK && $conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0); if ($bBorderLeft == 0 || $bBorderRight == 0 || $bBorderTop == 0 || $bBorderBottom == 0) { $bFormatBorder = 1; } else { $bFormatBorder = 0; } // Pattern $bFillStyle = ($conditional->getStyle()->getFill()->getFillType() === null ? 0 : 1); $bFillColor = ($conditional->getStyle()->getFill()->getStartColor()->getARGB() == null ? 0 : 1); $bFillColorBg = ($conditional->getStyle()->getFill()->getEndColor()->getARGB() == null ? 0 : 1); if ($bFillStyle == 0 || $bFillColor == 0 || $bFillColorBg == 0) { $bFormatFill = 1; } else { $bFormatFill = 0; } // Font if ( $conditional->getStyle()->getFont()->getName() !== null || $conditional->getStyle()->getFont()->getSize() !== null || $conditional->getStyle()->getFont()->getBold() !== null || $conditional->getStyle()->getFont()->getItalic() !== null || $conditional->getStyle()->getFont()->getSuperscript() !== null || $conditional->getStyle()->getFont()->getSubscript() !== null || $conditional->getStyle()->getFont()->getUnderline() !== null || $conditional->getStyle()->getFont()->getStrikethrough() !== null || $conditional->getStyle()->getFont()->getColor()->getARGB() != null ) { $bFormatFont = 1; } else { $bFormatFont = 0; } // Alignment $flags = 0; $flags |= (1 == $bAlignHz ? 0x00000001 : 0); $flags |= (1 == $bAlignVt ? 0x00000002 : 0); $flags |= (1 == $bAlignWrapTx ? 0x00000004 : 0); $flags |= (1 == $bTxRotation ? 0x00000008 : 0); // Justify last line flag $flags |= (1 == 1 ? 0x00000010 : 0); $flags |= (1 == $bIndent ? 0x00000020 : 0); $flags |= (1 == $bShrinkToFit ? 0x00000040 : 0); // Default $flags |= (1 == 1 ? 0x00000080 : 0); // Protection $flags |= (1 == $bProtLocked ? 0x00000100 : 0); $flags |= (1 == $bProtHidden ? 0x00000200 : 0); // Border $flags |= (1 == $bBorderLeft ? 0x00000400 : 0); $flags |= (1 == $bBorderRight ? 0x00000800 : 0); $flags |= (1 == $bBorderTop ? 0x00001000 : 0); $flags |= (1 == $bBorderBottom ? 0x00002000 : 0); $flags |= (1 == 1 ? 0x00004000 : 0); // Top left to Bottom right border $flags |= (1 == 1 ? 0x00008000 : 0); // Bottom left to Top right border // Pattern $flags |= (1 == $bFillStyle ? 0x00010000 : 0); $flags |= (1 == $bFillColor ? 0x00020000 : 0); $flags |= (1 == $bFillColorBg ? 0x00040000 : 0); $flags |= (1 == 1 ? 0x00380000 : 0); // Font $flags |= (1 == $bFormatFont ? 0x04000000 : 0); // Alignment: $flags |= (1 == $bFormatAlign ? 0x08000000 : 0); // Border $flags |= (1 == $bFormatBorder ? 0x10000000 : 0); // Pattern $flags |= (1 == $bFormatFill ? 0x20000000 : 0); // Protection $flags |= (1 == $bFormatProt ? 0x40000000 : 0); // Text direction $flags |= (1 == 0 ? 0x80000000 : 0); $dataBlockFont = null; $dataBlockAlign = null; $dataBlockBorder = null; $dataBlockFill = null; // Data Blocks if ($bFormatFont == 1) { // Font Name if ($conditional->getStyle()->getFont()->getName() === null) { $dataBlockFont = pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000); $dataBlockFont .= pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000); } else { $dataBlockFont = StringHelper::UTF8toBIFF8UnicodeLong($conditional->getStyle()->getFont()->getName()); } // Font Size if ($conditional->getStyle()->getFont()->getSize() === null) { $dataBlockFont .= pack('V', 20 * 11); } else { $dataBlockFont .= pack('V', 20 * $conditional->getStyle()->getFont()->getSize()); } // Font Options $dataBlockFont .= pack('V', 0); // Font weight if ($conditional->getStyle()->getFont()->getBold() === true) { $dataBlockFont .= pack('v', 0x02BC); } else { $dataBlockFont .= pack('v', 0x0190); } // Escapement type if ($conditional->getStyle()->getFont()->getSubscript() === true) { $dataBlockFont .= pack('v', 0x02); $fontEscapement = 0; } elseif ($conditional->getStyle()->getFont()->getSuperscript() === true) { $dataBlockFont .= pack('v', 0x01); $fontEscapement = 0; } else { $dataBlockFont .= pack('v', 0x00); $fontEscapement = 1; } // Underline type switch ($conditional->getStyle()->getFont()->getUnderline()) { case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE: $dataBlockFont .= pack('C', 0x00); $fontUnderline = 0; break; case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE: $dataBlockFont .= pack('C', 0x02); $fontUnderline = 0; break; case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLEACCOUNTING: $dataBlockFont .= pack('C', 0x22); $fontUnderline = 0; break; case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE: $dataBlockFont .= pack('C', 0x01); $fontUnderline = 0; break; case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLEACCOUNTING: $dataBlockFont .= pack('C', 0x21); $fontUnderline = 0; break; default: $dataBlockFont .= pack('C', 0x00); $fontUnderline = 1; break; } // Not used (3) $dataBlockFont .= pack('vC', 0x0000, 0x00); // Font color index $colorIdx = Style\ColorMap::lookup($conditional->getStyle()->getFont()->getColor(), 0x00); $dataBlockFont .= pack('V', $colorIdx); // Not used (4) $dataBlockFont .= pack('V', 0x00000000); // Options flags for modified font attributes $optionsFlags = 0; $optionsFlagsBold = ($conditional->getStyle()->getFont()->getBold() === null ? 1 : 0); $optionsFlags |= (1 == $optionsFlagsBold ? 0x00000002 : 0); $optionsFlags |= (1 == 1 ? 0x00000008 : 0); $optionsFlags |= (1 == 1 ? 0x00000010 : 0); $optionsFlags |= (1 == 0 ? 0x00000020 : 0); $optionsFlags |= (1 == 1 ? 0x00000080 : 0); $dataBlockFont .= pack('V', $optionsFlags); // Escapement type $dataBlockFont .= pack('V', $fontEscapement); // Underline type $dataBlockFont .= pack('V', $fontUnderline); // Always $dataBlockFont .= pack('V', 0x00000000); // Always $dataBlockFont .= pack('V', 0x00000000); // Not used (8) $dataBlockFont .= pack('VV', 0x00000000, 0x00000000); // Always $dataBlockFont .= pack('v', 0x0001); } if ($bFormatAlign === 1) { // Alignment and text break $blockAlign = Style\CellAlignment::horizontal($conditional->getStyle()->getAlignment()); $blockAlign |= Style\CellAlignment::wrap($conditional->getStyle()->getAlignment()) << 3; $blockAlign |= Style\CellAlignment::vertical($conditional->getStyle()->getAlignment()) << 4; $blockAlign |= 0 << 7; // Text rotation angle $blockRotation = $conditional->getStyle()->getAlignment()->getTextRotation(); // Indentation $blockIndent = $conditional->getStyle()->getAlignment()->getIndent(); if ($conditional->getStyle()->getAlignment()->getShrinkToFit() === true) { $blockIndent |= 1 << 4; } else { $blockIndent |= 0 << 4; } $blockIndent |= 0 << 6; // Relative indentation $blockIndentRelative = 255; $dataBlockAlign = pack('CCvvv', $blockAlign, $blockRotation, $blockIndent, $blockIndentRelative, 0x0000); } if ($bFormatBorder === 1) { $blockLineStyle = Style\CellBorder::style($conditional->getStyle()->getBorders()->getLeft()); $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getRight()) << 4; $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getTop()) << 8; $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getBottom()) << 12; // TODO writeCFRule() => $blockLineStyle => Index Color for left line // TODO writeCFRule() => $blockLineStyle => Index Color for right line // TODO writeCFRule() => $blockLineStyle => Top-left to bottom-right on/off // TODO writeCFRule() => $blockLineStyle => Bottom-left to top-right on/off $blockColor = 0; // TODO writeCFRule() => $blockColor => Index Color for top line // TODO writeCFRule() => $blockColor => Index Color for bottom line // TODO writeCFRule() => $blockColor => Index Color for diagonal line $blockColor |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getDiagonal()) << 21; $dataBlockBorder = pack('vv', $blockLineStyle, $blockColor); } if ($bFormatFill === 1) { // Fill Pattern Style $blockFillPatternStyle = Style\CellFill::style($conditional->getStyle()->getFill()); // Background Color $colorIdxBg = Style\ColorMap::lookup($conditional->getStyle()->getFill()->getStartColor(), 0x41); // Foreground Color $colorIdxFg = Style\ColorMap::lookup($conditional->getStyle()->getFill()->getEndColor(), 0x40); $dataBlockFill = pack('v', $blockFillPatternStyle); $dataBlockFill .= pack('v', $colorIdxFg | ($colorIdxBg << 7)); } $data = pack('CCvvVv', $type, $operatorType, $szValue1, $szValue2, $flags, 0x0000); if ($bFormatFont === 1) { // Block Formatting : OK $data .= $dataBlockFont; } if ($bFormatAlign === 1) { $data .= $dataBlockAlign; } if ($bFormatBorder === 1) { $data .= $dataBlockBorder; } if ($bFormatFill === 1) { // Block Formatting : OK $data .= $dataBlockFill; } if ($bFormatProt == 1) { $data .= $this->getDataBlockProtection($conditional); } if ($operand1 !== null) { $data .= $operand1; } if ($operand2 !== null) { $data .= $operand2; } $header = pack('vv', $record, strlen($data)); $this->append($header . $data); } /** * Write CFHeader record. */ private function writeCFHeader(): void { $record = 0x01B0; // Record identifier $length = 0x0016; // Bytes to follow $numColumnMin = null; $numColumnMax = null; $numRowMin = null; $numRowMax = null; $arrConditional = []; foreach ($this->phpSheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) { foreach ($conditionalStyles as $conditional) { if ( $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION || $conditional->getConditionType() == Conditional::CONDITION_CELLIS ) { if (!in_array($conditional->getHashCode(), $arrConditional)) { $arrConditional[] = $conditional->getHashCode(); } // Cells $rangeCoordinates = Coordinate::rangeBoundaries($cellCoordinate); if ($numColumnMin === null || ($numColumnMin > $rangeCoordinates[0][0])) { $numColumnMin = $rangeCoordinates[0][0]; } if ($numColumnMax === null || ($numColumnMax < $rangeCoordinates[1][0])) { $numColumnMax = $rangeCoordinates[1][0]; } if ($numRowMin === null || ($numRowMin > $rangeCoordinates[0][1])) { $numRowMin = (int) $rangeCoordinates[0][1]; } if ($numRowMax === null || ($numRowMax < $rangeCoordinates[1][1])) { $numRowMax = (int) $rangeCoordinates[1][1]; } } } } $needRedraw = 1; $cellRange = pack('vvvv', $numRowMin - 1, $numRowMax - 1, $numColumnMin - 1, $numColumnMax - 1); $header = pack('vv', $record, $length); $data = pack('vv', count($arrConditional), $needRedraw); $data .= $cellRange; $data .= pack('v', 0x0001); $data .= $cellRange; $this->append($header . $data); } private function getDataBlockProtection(Conditional $conditional): int { $dataBlockProtection = 0; if ($conditional->getStyle()->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED) { $dataBlockProtection = 1; } if ($conditional->getStyle()->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED) { $dataBlockProtection = 1 << 1; } return $dataBlockProtection; } } PK[*aBIFFwriter.phpnu[ // * // * The majority of this is _NOT_ my code. I simply ported it from the // * PERL Spreadsheet::WriteExcel module. // * // * The author of the Spreadsheet::WriteExcel module is John McNamara // * // * // * I _DO_ maintain this code, and John McNamara has nothing to do with the // * porting of this code to PHP. Any questions directly related to this // * class library should be directed to me. // * // * License Information: // * // * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets // * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com // * // * This library is free software; you can redistribute it and/or // * modify it under the terms of the GNU Lesser General Public // * License as published by the Free Software Foundation; either // * version 2.1 of the License, or (at your option) any later version. // * // * This library is distributed in the hope that it will be useful, // * but WITHOUT ANY WARRANTY; without even the implied warranty of // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // * Lesser General Public License for more details. // * // * You should have received a copy of the GNU Lesser General Public // * License along with this library; if not, write to the Free Software // * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // */ class BIFFwriter { /** * The byte order of this architecture. 0 => little endian, 1 => big endian. * * @var int */ private static $byteOrder; /** * The string containing the data of the BIFF stream. * * @var null|string */ public $_data; /** * The size of the data in bytes. Should be the same as strlen($this->_data). * * @var int */ public $_datasize; /** * The maximum length for a BIFF record (excluding record header and length field). See addContinue(). * * @var int * * @see addContinue() */ private $limit = 8224; /** * Constructor. */ public function __construct() { $this->_data = ''; $this->_datasize = 0; } /** * Determine the byte order and store it as class data to avoid * recalculating it for each call to new(). * * @return int */ public static function getByteOrder() { if (!isset(self::$byteOrder)) { // Check if "pack" gives the required IEEE 64bit float $teststr = pack('d', 1.2345); $number = pack('C8', 0x8D, 0x97, 0x6E, 0x12, 0x83, 0xC0, 0xF3, 0x3F); if ($number == $teststr) { $byte_order = 0; // Little Endian } elseif ($number == strrev($teststr)) { $byte_order = 1; // Big Endian } else { // Give up. I'll fix this in a later version. throw new WriterException('Required floating point format not supported on this platform.'); } self::$byteOrder = $byte_order; } return self::$byteOrder; } /** * General storage function. * * @param string $data binary data to append */ protected function append($data): void { if (strlen($data) - 4 > $this->limit) { $data = $this->addContinue($data); } $this->_data .= $data; $this->_datasize += strlen($data); } /** * General storage function like append, but returns string instead of modifying $this->_data. * * @param string $data binary data to write * * @return string */ public function writeData($data) { if (strlen($data) - 4 > $this->limit) { $data = $this->addContinue($data); } $this->_datasize += strlen($data); return $data; } /** * Writes Excel BOF record to indicate the beginning of a stream or * sub-stream in the BIFF file. * * @param int $type type of BIFF file to write: 0x0005 Workbook, * 0x0010 Worksheet */ protected function storeBof($type): void { $record = 0x0809; // Record identifier (BIFF5-BIFF8) $length = 0x0010; // by inspection of real files, MS Office Excel 2007 writes the following $unknown = pack('VV', 0x000100D1, 0x00000406); $build = 0x0DBB; // Excel 97 $year = 0x07CC; // Excel 97 $version = 0x0600; // BIFF8 $header = pack('vv', $record, $length); $data = pack('vvvv', $version, $type, $build, $year); $this->append($header . $data . $unknown); } /** * Writes Excel EOF record to indicate the end of a BIFF stream. */ protected function storeEof(): void { $record = 0x000A; // Record identifier $length = 0x0000; // Number of bytes to follow $header = pack('vv', $record, $length); $this->append($header); } /** * Writes Excel EOF record to indicate the end of a BIFF stream. */ public function writeEof() { $record = 0x000A; // Record identifier $length = 0x0000; // Number of bytes to follow $header = pack('vv', $record, $length); return $this->writeData($header); } /** * Excel limits the size of BIFF records. In Excel 5 the limit is 2084 bytes. In * Excel 97 the limit is 8228 bytes. Records that are longer than these limits * must be split up into CONTINUE blocks. * * This function takes a long BIFF record and inserts CONTINUE records as * necessary. * * @param string $data The original binary data to be written * * @return string A very convenient string of continue blocks */ private function addContinue($data) { $limit = $this->limit; $record = 0x003C; // Record identifier // The first 2080/8224 bytes remain intact. However, we have to change // the length field of the record. $tmp = substr($data, 0, 2) . pack('v', $limit) . substr($data, 4, $limit); $header = pack('vv', $record, $limit); // Headers for continue records // Retrieve chunks of 2080/8224 bytes +4 for the header. $data_length = strlen($data); for ($i = $limit + 4; $i < ($data_length - $limit); $i += $limit) { $tmp .= $header; $tmp .= substr($data, $i, $limit); } // Retrieve the last chunk of data $header = pack('vv', $record, strlen($data) - $i); $tmp .= $header; $tmp .= substr($data, $i); return $tmp; } } PK[(00Xf.phpnu[ // * // * The majority of this is _NOT_ my code. I simply ported it from the // * PERL Spreadsheet::WriteExcel module. // * // * The author of the Spreadsheet::WriteExcel module is John McNamara // * // * // * I _DO_ maintain this code, and John McNamara has nothing to do with the // * porting of this code to PHP. Any questions directly related to this // * class library should be directed to me. // * // * License Information: // * // * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets // * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com // * // * This library is free software; you can redistribute it and/or // * modify it under the terms of the GNU Lesser General Public // * License as published by the Free Software Foundation; either // * version 2.1 of the License, or (at your option) any later version. // * // * This library is distributed in the hope that it will be useful, // * but WITHOUT ANY WARRANTY; without even the implied warranty of // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // * Lesser General Public License for more details. // * // * You should have received a copy of the GNU Lesser General Public // * License along with this library; if not, write to the Free Software // * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // */ class Xf { /** * Style XF or a cell XF ? * * @var bool */ private $isStyleXf; /** * Index to the FONT record. Index 4 does not exist. * * @var int */ private $fontIndex; /** * An index (2 bytes) to a FORMAT record (number format). * * @var int */ private $numberFormatIndex; /** * 1 bit, apparently not used. * * @var int */ private $textJustLast; /** * The cell's foreground color. * * @var int */ private $foregroundColor; /** * The cell's background color. * * @var int */ private $backgroundColor; /** * Color of the bottom border of the cell. * * @var int */ private $bottomBorderColor; /** * Color of the top border of the cell. * * @var int */ private $topBorderColor; /** * Color of the left border of the cell. * * @var int */ private $leftBorderColor; /** * Color of the right border of the cell. * * @var int */ private $rightBorderColor; /** * @var int */ private $diag; /** * @var int */ private $diagColor; /** * @var Style */ private $style; /** * Constructor. * * @param Style $style The XF format */ public function __construct(Style $style) { $this->isStyleXf = false; $this->fontIndex = 0; $this->numberFormatIndex = 0; $this->textJustLast = 0; $this->foregroundColor = 0x40; $this->backgroundColor = 0x41; $this->diag = 0; $this->bottomBorderColor = 0x40; $this->topBorderColor = 0x40; $this->leftBorderColor = 0x40; $this->rightBorderColor = 0x40; $this->diagColor = 0x40; $this->style = $style; } /** * Generate an Excel BIFF XF record (style or cell). * * @return string The XF record */ public function writeXf() { // Set the type of the XF record and some of the attributes. if ($this->isStyleXf) { $style = 0xFFF5; } else { $style = self::mapLocked($this->style->getProtection()->getLocked()); $style |= self::mapHidden($this->style->getProtection()->getHidden()) << 1; } // Flags to indicate if attributes have been set. $atr_num = ($this->numberFormatIndex != 0) ? 1 : 0; $atr_fnt = ($this->fontIndex != 0) ? 1 : 0; $atr_alc = ((int) $this->style->getAlignment()->getWrapText()) ? 1 : 0; $atr_bdr = (CellBorder::style($this->style->getBorders()->getBottom()) || CellBorder::style($this->style->getBorders()->getTop()) || CellBorder::style($this->style->getBorders()->getLeft()) || CellBorder::style($this->style->getBorders()->getRight())) ? 1 : 0; $atr_pat = ($this->foregroundColor != 0x40) ? 1 : 0; $atr_pat = ($this->backgroundColor != 0x41) ? 1 : $atr_pat; $atr_pat = CellFill::style($this->style->getFill()) ? 1 : $atr_pat; $atr_prot = self::mapLocked($this->style->getProtection()->getLocked()) | self::mapHidden($this->style->getProtection()->getHidden()); // Zero the default border colour if the border has not been set. if (CellBorder::style($this->style->getBorders()->getBottom()) == 0) { $this->bottomBorderColor = 0; } if (CellBorder::style($this->style->getBorders()->getTop()) == 0) { $this->topBorderColor = 0; } if (CellBorder::style($this->style->getBorders()->getRight()) == 0) { $this->rightBorderColor = 0; } if (CellBorder::style($this->style->getBorders()->getLeft()) == 0) { $this->leftBorderColor = 0; } if (CellBorder::style($this->style->getBorders()->getDiagonal()) == 0) { $this->diagColor = 0; } $record = 0x00E0; // Record identifier $length = 0x0014; // Number of bytes to follow $ifnt = $this->fontIndex; // Index to FONT record $ifmt = $this->numberFormatIndex; // Index to FORMAT record // Alignment $align = CellAlignment::horizontal($this->style->getAlignment()); $align |= CellAlignment::wrap($this->style->getAlignment()) << 3; $align |= CellAlignment::vertical($this->style->getAlignment()) << 4; $align |= $this->textJustLast << 7; $used_attrib = $atr_num << 2; $used_attrib |= $atr_fnt << 3; $used_attrib |= $atr_alc << 4; $used_attrib |= $atr_bdr << 5; $used_attrib |= $atr_pat << 6; $used_attrib |= $atr_prot << 7; $icv = $this->foregroundColor; // fg and bg pattern colors $icv |= $this->backgroundColor << 7; $border1 = CellBorder::style($this->style->getBorders()->getLeft()); // Border line style and color $border1 |= CellBorder::style($this->style->getBorders()->getRight()) << 4; $border1 |= CellBorder::style($this->style->getBorders()->getTop()) << 8; $border1 |= CellBorder::style($this->style->getBorders()->getBottom()) << 12; $border1 |= $this->leftBorderColor << 16; $border1 |= $this->rightBorderColor << 23; $diagonalDirection = $this->style->getBorders()->getDiagonalDirection(); $diag_tl_to_rb = $diagonalDirection == Borders::DIAGONAL_BOTH || $diagonalDirection == Borders::DIAGONAL_DOWN; $diag_tr_to_lb = $diagonalDirection == Borders::DIAGONAL_BOTH || $diagonalDirection == Borders::DIAGONAL_UP; $border1 |= $diag_tl_to_rb << 30; $border1 |= $diag_tr_to_lb << 31; $border2 = $this->topBorderColor; // Border color $border2 |= $this->bottomBorderColor << 7; $border2 |= $this->diagColor << 14; $border2 |= CellBorder::style($this->style->getBorders()->getDiagonal()) << 21; $border2 |= CellFill::style($this->style->getFill()) << 26; $header = pack('vv', $record, $length); //BIFF8 options: identation, shrinkToFit and text direction $biff8_options = $this->style->getAlignment()->getIndent(); $biff8_options |= (int) $this->style->getAlignment()->getShrinkToFit() << 4; $data = pack('vvvC', $ifnt, $ifmt, $style, $align); $data .= pack('CCC', self::mapTextRotation($this->style->getAlignment()->getTextRotation()), $biff8_options, $used_attrib); $data .= pack('VVv', $border1, $border2, $icv); return $header . $data; } /** * Is this a style XF ? * * @param bool $value */ public function setIsStyleXf($value): void { $this->isStyleXf = $value; } /** * Sets the cell's bottom border color. * * @param int $colorIndex Color index */ public function setBottomColor($colorIndex): void { $this->bottomBorderColor = $colorIndex; } /** * Sets the cell's top border color. * * @param int $colorIndex Color index */ public function setTopColor($colorIndex): void { $this->topBorderColor = $colorIndex; } /** * Sets the cell's left border color. * * @param int $colorIndex Color index */ public function setLeftColor($colorIndex): void { $this->leftBorderColor = $colorIndex; } /** * Sets the cell's right border color. * * @param int $colorIndex Color index */ public function setRightColor($colorIndex): void { $this->rightBorderColor = $colorIndex; } /** * Sets the cell's diagonal border color. * * @param int $colorIndex Color index */ public function setDiagColor($colorIndex): void { $this->diagColor = $colorIndex; } /** * Sets the cell's foreground color. * * @param int $colorIndex Color index */ public function setFgColor($colorIndex): void { $this->foregroundColor = $colorIndex; } /** * Sets the cell's background color. * * @param int $colorIndex Color index */ public function setBgColor($colorIndex): void { $this->backgroundColor = $colorIndex; } /** * Sets the index to the number format record * It can be date, time, currency, etc... * * @param int $numberFormatIndex Index to format record */ public function setNumberFormatIndex($numberFormatIndex): void { $this->numberFormatIndex = $numberFormatIndex; } /** * Set the font index. * * @param int $value Font index, note that value 4 does not exist */ public function setFontIndex($value): void { $this->fontIndex = $value; } /** * Map to BIFF8 codes for text rotation angle. * * @param int $textRotation * * @return int */ private static function mapTextRotation($textRotation) { if ($textRotation >= 0) { return $textRotation; } if ($textRotation == Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) { return Alignment::TEXTROTATION_STACK_EXCEL; } return 90 - $textRotation; } private const LOCK_ARRAY = [ Protection::PROTECTION_INHERIT => 1, Protection::PROTECTION_PROTECTED => 1, Protection::PROTECTION_UNPROTECTED => 0, ]; /** * Map locked values. * * @param string $locked * * @return int */ private static function mapLocked($locked) { return array_key_exists($locked, self::LOCK_ARRAY) ? self::LOCK_ARRAY[$locked] : 1; } private const HIDDEN_ARRAY = [ Protection::PROTECTION_INHERIT => 0, Protection::PROTECTION_PROTECTED => 1, Protection::PROTECTION_UNPROTECTED => 0, ]; /** * Map hidden. * * @param string $hidden * * @return int */ private static function mapHidden($hidden) { return array_key_exists($hidden, self::HIDDEN_ARRAY) ? self::HIDDEN_ARRAY[$hidden] : 0; } } PK[ Workbook.phpnu[ // * // * The majority of this is _NOT_ my code. I simply ported it from the // * PERL Spreadsheet::WriteExcel module. // * // * The author of the Spreadsheet::WriteExcel module is John McNamara // * // * // * I _DO_ maintain this code, and John McNamara has nothing to do with the // * porting of this code to PHP. Any questions directly related to this // * class library should be directed to me. // * // * License Information: // * // * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets // * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com // * // * This library is free software; you can redistribute it and/or // * modify it under the terms of the GNU Lesser General Public // * License as published by the Free Software Foundation; either // * version 2.1 of the License, or (at your option) any later version. // * // * This library is distributed in the hope that it will be useful, // * but WITHOUT ANY WARRANTY; without even the implied warranty of // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // * Lesser General Public License for more details. // * // * You should have received a copy of the GNU Lesser General Public // * License along with this library; if not, write to the Free Software // * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // */ class Workbook extends BIFFwriter { /** * Formula parser. * * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser */ private $parser; /** * The BIFF file size for the workbook. * * @var int * * @see calcSheetOffsets() */ private $biffSize; /** * XF Writers. * * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[] */ private $xfWriters = []; /** * Array containing the colour palette. * * @var array */ private $palette; /** * The codepage indicates the text encoding used for strings. * * @var int */ private $codepage; /** * The country code used for localization. * * @var int */ private $countryCode; /** * Workbook. * * @var Spreadsheet */ private $spreadsheet; /** * Fonts writers. * * @var Font[] */ private $fontWriters = []; /** * Added fonts. Maps from font's hash => index in workbook. * * @var array */ private $addedFonts = []; /** * Shared number formats. * * @var array */ private $numberFormats = []; /** * Added number formats. Maps from numberFormat's hash => index in workbook. * * @var array */ private $addedNumberFormats = []; /** * Sizes of the binary worksheet streams. * * @var array */ private $worksheetSizes = []; /** * Offsets of the binary worksheet streams relative to the start of the global workbook stream. * * @var array */ private $worksheetOffsets = []; /** * Total number of shared strings in workbook. * * @var int */ private $stringTotal; /** * Number of unique shared strings in workbook. * * @var int */ private $stringUnique; /** * Array of unique shared strings in workbook. * * @var array */ private $stringTable; /** * Color cache. */ private $colors; /** * Escher object corresponding to MSODRAWINGGROUP. * * @var null|\PhpOffice\PhpSpreadsheet\Shared\Escher */ private $escher; /** * Class constructor. * * @param Spreadsheet $spreadsheet The Workbook * @param int $str_total Total number of strings * @param int $str_unique Total number of unique strings * @param array $str_table String Table * @param array $colors Colour Table * @param Parser $parser The formula parser created for the Workbook */ public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser) { // It needs to call its parent's constructor explicitly parent::__construct(); $this->parser = $parser; $this->biffSize = 0; $this->palette = []; $this->countryCode = -1; $this->stringTotal = &$str_total; $this->stringUnique = &$str_unique; $this->stringTable = &$str_table; $this->colors = &$colors; $this->setPaletteXl97(); $this->spreadsheet = $spreadsheet; $this->codepage = 0x04B0; // Add empty sheets and Build color cache $countSheets = $spreadsheet->getSheetCount(); for ($i = 0; $i < $countSheets; ++$i) { $phpSheet = $spreadsheet->getSheet($i); $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser $supbook_index = 0x00; $ref = pack('vvv', $supbook_index, $i, $i); $this->parser->references[] = $ref; // Register reference with parser // Sheet tab colors? if ($phpSheet->isTabColorSet()) { $this->addColor($phpSheet->getTabColor()->getRGB()); } } } /** * Add a new XF writer. * * @param bool $isStyleXf Is it a style XF? * * @return int Index to XF record */ public function addXfWriter(Style $style, $isStyleXf = false) { $xfWriter = new Xf($style); $xfWriter->setIsStyleXf($isStyleXf); // Add the font if not already added $fontIndex = $this->addFont($style->getFont()); // Assign the font index to the xf record $xfWriter->setFontIndex($fontIndex); // Background colors, best to treat these after the font so black will come after white in custom palette $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB())); $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB())); $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB())); $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB())); $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB())); $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB())); $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB())); // Add the number format if it is not a built-in one and not already added if ($style->getNumberFormat()->getBuiltInFormatCode() === false) { $numberFormatHashCode = $style->getNumberFormat()->getHashCode(); if (isset($this->addedNumberFormats[$numberFormatHashCode])) { $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode]; } else { $numberFormatIndex = 164 + count($this->numberFormats); $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat(); $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex; } } else { $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode(); } // Assign the number format index to xf record $xfWriter->setNumberFormatIndex($numberFormatIndex); $this->xfWriters[] = $xfWriter; return count($this->xfWriters) - 1; } /** * Add a font to added fonts. * * @return int Index to FONT record */ public function addFont(\PhpOffice\PhpSpreadsheet\Style\Font $font) { $fontHashCode = $font->getHashCode(); if (isset($this->addedFonts[$fontHashCode])) { $fontIndex = $this->addedFonts[$fontHashCode]; } else { $countFonts = count($this->fontWriters); $fontIndex = ($countFonts < 4) ? $countFonts : $countFonts + 1; $fontWriter = new Font($font); $fontWriter->setColorIndex($this->addColor($font->getColor()->getRGB())); $this->fontWriters[] = $fontWriter; $this->addedFonts[$fontHashCode] = $fontIndex; } return $fontIndex; } /** * Alter color palette adding a custom color. * * @param string $rgb E.g. 'FF00AA' * * @return int Color index */ private function addColor($rgb) { if (!isset($this->colors[$rgb])) { $color = [ hexdec(substr($rgb, 0, 2)), hexdec(substr($rgb, 2, 2)), hexdec(substr($rgb, 4)), 0, ]; $colorIndex = array_search($color, $this->palette); if ($colorIndex) { $this->colors[$rgb] = $colorIndex; } else { if (count($this->colors) === 0) { $lastColor = 7; } else { $lastColor = end($this->colors); } if ($lastColor < 57) { // then we add a custom color altering the palette $colorIndex = $lastColor + 1; $this->palette[$colorIndex] = $color; $this->colors[$rgb] = $colorIndex; } else { // no room for more custom colors, just map to black $colorIndex = 0; } } } else { // fetch already added custom color $colorIndex = $this->colors[$rgb]; } return $colorIndex; } /** * Sets the colour palette to the Excel 97+ default. */ private function setPaletteXl97(): void { $this->palette = [ 0x08 => [0x00, 0x00, 0x00, 0x00], 0x09 => [0xff, 0xff, 0xff, 0x00], 0x0A => [0xff, 0x00, 0x00, 0x00], 0x0B => [0x00, 0xff, 0x00, 0x00], 0x0C => [0x00, 0x00, 0xff, 0x00], 0x0D => [0xff, 0xff, 0x00, 0x00], 0x0E => [0xff, 0x00, 0xff, 0x00], 0x0F => [0x00, 0xff, 0xff, 0x00], 0x10 => [0x80, 0x00, 0x00, 0x00], 0x11 => [0x00, 0x80, 0x00, 0x00], 0x12 => [0x00, 0x00, 0x80, 0x00], 0x13 => [0x80, 0x80, 0x00, 0x00], 0x14 => [0x80, 0x00, 0x80, 0x00], 0x15 => [0x00, 0x80, 0x80, 0x00], 0x16 => [0xc0, 0xc0, 0xc0, 0x00], 0x17 => [0x80, 0x80, 0x80, 0x00], 0x18 => [0x99, 0x99, 0xff, 0x00], 0x19 => [0x99, 0x33, 0x66, 0x00], 0x1A => [0xff, 0xff, 0xcc, 0x00], 0x1B => [0xcc, 0xff, 0xff, 0x00], 0x1C => [0x66, 0x00, 0x66, 0x00], 0x1D => [0xff, 0x80, 0x80, 0x00], 0x1E => [0x00, 0x66, 0xcc, 0x00], 0x1F => [0xcc, 0xcc, 0xff, 0x00], 0x20 => [0x00, 0x00, 0x80, 0x00], 0x21 => [0xff, 0x00, 0xff, 0x00], 0x22 => [0xff, 0xff, 0x00, 0x00], 0x23 => [0x00, 0xff, 0xff, 0x00], 0x24 => [0x80, 0x00, 0x80, 0x00], 0x25 => [0x80, 0x00, 0x00, 0x00], 0x26 => [0x00, 0x80, 0x80, 0x00], 0x27 => [0x00, 0x00, 0xff, 0x00], 0x28 => [0x00, 0xcc, 0xff, 0x00], 0x29 => [0xcc, 0xff, 0xff, 0x00], 0x2A => [0xcc, 0xff, 0xcc, 0x00], 0x2B => [0xff, 0xff, 0x99, 0x00], 0x2C => [0x99, 0xcc, 0xff, 0x00], 0x2D => [0xff, 0x99, 0xcc, 0x00], 0x2E => [0xcc, 0x99, 0xff, 0x00], 0x2F => [0xff, 0xcc, 0x99, 0x00], 0x30 => [0x33, 0x66, 0xff, 0x00], 0x31 => [0x33, 0xcc, 0xcc, 0x00], 0x32 => [0x99, 0xcc, 0x00, 0x00], 0x33 => [0xff, 0xcc, 0x00, 0x00], 0x34 => [0xff, 0x99, 0x00, 0x00], 0x35 => [0xff, 0x66, 0x00, 0x00], 0x36 => [0x66, 0x66, 0x99, 0x00], 0x37 => [0x96, 0x96, 0x96, 0x00], 0x38 => [0x00, 0x33, 0x66, 0x00], 0x39 => [0x33, 0x99, 0x66, 0x00], 0x3A => [0x00, 0x33, 0x00, 0x00], 0x3B => [0x33, 0x33, 0x00, 0x00], 0x3C => [0x99, 0x33, 0x00, 0x00], 0x3D => [0x99, 0x33, 0x66, 0x00], 0x3E => [0x33, 0x33, 0x99, 0x00], 0x3F => [0x33, 0x33, 0x33, 0x00], ]; } /** * Assemble worksheets into a workbook and send the BIFF data to an OLE * storage. * * @param array $worksheetSizes The sizes in bytes of the binary worksheet streams * * @return string Binary data for workbook stream */ public function writeWorkbook(array $worksheetSizes) { $this->worksheetSizes = $worksheetSizes; // Calculate the number of selected worksheet tabs and call the finalization // methods for each worksheet $total_worksheets = $this->spreadsheet->getSheetCount(); // Add part 1 of the Workbook globals, what goes before the SHEET records $this->storeBof(0x0005); $this->writeCodepage(); $this->writeWindow1(); $this->writeDateMode(); $this->writeAllFonts(); $this->writeAllNumberFormats(); $this->writeAllXfs(); $this->writeAllStyles(); $this->writePalette(); // Prepare part 3 of the workbook global stream, what goes after the SHEET records $part3 = ''; if ($this->countryCode !== -1) { $part3 .= $this->writeCountry(); } $part3 .= $this->writeRecalcId(); $part3 .= $this->writeSupbookInternal(); /* TODO: store external SUPBOOK records and XCT and CRN records in case of external references for BIFF8 */ $part3 .= $this->writeExternalsheetBiff8(); $part3 .= $this->writeAllDefinedNamesBiff8(); $part3 .= $this->writeMsoDrawingGroup(); $part3 .= $this->writeSharedStringsTable(); $part3 .= $this->writeEof(); // Add part 2 of the Workbook globals, the SHEET records $this->calcSheetOffsets(); for ($i = 0; $i < $total_worksheets; ++$i) { $this->writeBoundSheet($this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]); } // Add part 3 of the Workbook globals $this->_data .= $part3; return $this->_data; } /** * Calculate offsets for Worksheet BOF records. */ private function calcSheetOffsets(): void { $boundsheet_length = 10; // fixed length for a BOUNDSHEET record // size of Workbook globals part 1 + 3 $offset = $this->_datasize; // add size of Workbook globals part 2, the length of the SHEET records $total_worksheets = count($this->spreadsheet->getAllSheets()); foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) { $offset += $boundsheet_length + strlen(StringHelper::UTF8toBIFF8UnicodeShort($sheet->getTitle())); } // add the sizes of each of the Sheet substreams, respectively for ($i = 0; $i < $total_worksheets; ++$i) { $this->worksheetOffsets[$i] = $offset; $offset += $this->worksheetSizes[$i]; } $this->biffSize = $offset; } /** * Store the Excel FONT records. */ private function writeAllFonts(): void { foreach ($this->fontWriters as $fontWriter) { $this->append($fontWriter->writeFont()); } } /** * Store user defined numerical formats i.e. FORMAT records. */ private function writeAllNumberFormats(): void { foreach ($this->numberFormats as $numberFormatIndex => $numberFormat) { $this->writeNumberFormat($numberFormat->getFormatCode(), $numberFormatIndex); } } /** * Write all XF records. */ private function writeAllXfs(): void { foreach ($this->xfWriters as $xfWriter) { $this->append($xfWriter->writeXf()); } } /** * Write all STYLE records. */ private function writeAllStyles(): void { $this->writeStyle(); } private function parseDefinedNameValue(DefinedName $definedName): string { $definedRange = $definedName->getValue(); $splitCount = preg_match_all( '/' . Calculation::CALCULATION_REGEXP_CELLREF . '/mui', $definedRange, $splitRanges, PREG_OFFSET_CAPTURE ); $lengths = array_map('strlen', array_column($splitRanges[0], 0)); $offsets = array_column($splitRanges[0], 1); $worksheets = $splitRanges[2]; $columns = $splitRanges[6]; $rows = $splitRanges[7]; while ($splitCount > 0) { --$splitCount; $length = $lengths[$splitCount]; $offset = $offsets[$splitCount]; $worksheet = $worksheets[$splitCount][0]; $column = $columns[$splitCount][0]; $row = $rows[$splitCount][0]; $newRange = ''; if (empty($worksheet)) { if (($offset === 0) || ($definedRange[$offset - 1] !== ':')) { // We should have a worksheet $worksheet = $definedName->getWorksheet() ? $definedName->getWorksheet()->getTitle() : null; } } else { $worksheet = str_replace("''", "'", trim($worksheet, "'")); } if (!empty($worksheet)) { $newRange = "'" . str_replace("'", "''", $worksheet) . "'!"; } if (!empty($column)) { $newRange .= "\${$column}"; } if (!empty($row)) { $newRange .= "\${$row}"; } $definedRange = substr($definedRange, 0, $offset) . $newRange . substr($definedRange, $offset + $length); } return $definedRange; } /** * Writes all the DEFINEDNAME records (BIFF8). * So far this is only used for repeating rows/columns (print titles) and print areas. */ private function writeAllDefinedNamesBiff8() { $chunk = ''; // Named ranges $definedNames = $this->spreadsheet->getDefinedNames(); if (count($definedNames) > 0) { // Loop named ranges foreach ($definedNames as $definedName) { $range = $this->parseDefinedNameValue($definedName); // parse formula try { $error = $this->parser->parse($range); $formulaData = $this->parser->toReversePolish(); // make sure tRef3d is of type tRef3dR (0x3A) if (isset($formulaData[0]) && ($formulaData[0] == "\x7A" || $formulaData[0] == "\x5A")) { $formulaData = "\x3A" . substr($formulaData, 1); } if ($definedName->getLocalOnly()) { /** * local scope. * * @phpstan-ignore-next-line */ $scope = $this->spreadsheet->getIndex($definedName->getScope()) + 1; } else { // global scope $scope = 0; } $chunk .= $this->writeData($this->writeDefinedNameBiff8($definedName->getName(), $formulaData, $scope, false)); } catch (PhpSpreadsheetException $e) { // do nothing } } } // total number of sheets $total_worksheets = $this->spreadsheet->getSheetCount(); // write the print titles (repeating rows, columns), if any for ($i = 0; $i < $total_worksheets; ++$i) { $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup(); // simultaneous repeatColumns repeatRows if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) { $repeat = $sheetSetup->getColumnsToRepeatAtLeft(); $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1; $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1; $repeat = $sheetSetup->getRowsToRepeatAtTop(); $rowmin = $repeat[0] - 1; $rowmax = $repeat[1] - 1; // construct formula data manually $formulaData = pack('Cv', 0x29, 0x17); // tMemFunc $formulaData .= pack('Cvvvvv', 0x3B, $i, 0, 65535, $colmin, $colmax); // tArea3d $formulaData .= pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, 0, 255); // tArea3d $formulaData .= pack('C', 0x10); // tList // store the DEFINEDNAME record $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true)); // (exclusive) either repeatColumns or repeatRows } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) { // Columns to repeat if ($sheetSetup->isColumnsToRepeatAtLeftSet()) { $repeat = $sheetSetup->getColumnsToRepeatAtLeft(); $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1; $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1; } else { $colmin = 0; $colmax = 255; } // Rows to repeat if ($sheetSetup->isRowsToRepeatAtTopSet()) { $repeat = $sheetSetup->getRowsToRepeatAtTop(); $rowmin = $repeat[0] - 1; $rowmax = $repeat[1] - 1; } else { $rowmin = 0; $rowmax = 65535; } // construct formula data manually because parser does not recognize absolute 3d cell references $formulaData = pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, $colmin, $colmax); // store the DEFINEDNAME record $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true)); } } // write the print areas, if any for ($i = 0; $i < $total_worksheets; ++$i) { $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup(); if ($sheetSetup->isPrintAreaSet()) { // Print area, e.g. A3:J6,H1:X20 $printArea = Coordinate::splitRange($sheetSetup->getPrintArea()); $countPrintArea = count($printArea); $formulaData = ''; for ($j = 0; $j < $countPrintArea; ++$j) { $printAreaRect = $printArea[$j]; // e.g. A3:J6 $printAreaRect[0] = Coordinate::indexesFromString($printAreaRect[0]); $printAreaRect[1] = Coordinate::indexesFromString($printAreaRect[1]); $print_rowmin = $printAreaRect[0][1] - 1; $print_rowmax = $printAreaRect[1][1] - 1; $print_colmin = $printAreaRect[0][0] - 1; $print_colmax = $printAreaRect[1][0] - 1; // construct formula data manually because parser does not recognize absolute 3d cell references $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax); if ($j > 0) { $formulaData .= pack('C', 0x10); // list operator token ',' } } // store the DEFINEDNAME record $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x06), $formulaData, $i + 1, true)); } } // write autofilters, if any for ($i = 0; $i < $total_worksheets; ++$i) { $sheetAutoFilter = $this->spreadsheet->getSheet($i)->getAutoFilter(); $autoFilterRange = $sheetAutoFilter->getRange(); if (!empty($autoFilterRange)) { $rangeBounds = Coordinate::rangeBoundaries($autoFilterRange); //Autofilter built in name $name = pack('C', 0x0D); $chunk .= $this->writeData($this->writeShortNameBiff8($name, $i + 1, $rangeBounds, true)); } } return $chunk; } /** * Write a DEFINEDNAME record for BIFF8 using explicit binary formula data. * * @param string $name The name in UTF-8 * @param string $formulaData The binary formula data * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global * @param bool $isBuiltIn Built-in name? * * @return string Complete binary record data */ private function writeDefinedNameBiff8($name, $formulaData, $sheetIndex = 0, $isBuiltIn = false) { $record = 0x0018; // option flags $options = $isBuiltIn ? 0x20 : 0x00; // length of the name, character count $nlen = StringHelper::countCharacters($name); // name with stripped length field $name = substr(StringHelper::UTF8toBIFF8UnicodeLong($name), 2); // size of the formula (in bytes) $sz = strlen($formulaData); // combine the parts $data = pack('vCCvvvCCCC', $options, 0, $nlen, $sz, 0, $sheetIndex, 0, 0, 0, 0) . $name . $formulaData; $length = strlen($data); $header = pack('vv', $record, $length); return $header . $data; } /** * Write a short NAME record. * * @param string $name * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global * @param int[][] $rangeBounds range boundaries * @param bool $isHidden * * @return string Complete binary record data * */ private function writeShortNameBiff8($name, $sheetIndex, $rangeBounds, $isHidden = false) { $record = 0x0018; // option flags $options = ($isHidden ? 0x21 : 0x00); $extra = pack( 'Cvvvvv', 0x3B, $sheetIndex - 1, $rangeBounds[0][1] - 1, $rangeBounds[1][1] - 1, $rangeBounds[0][0] - 1, $rangeBounds[1][0] - 1 ); // size of the formula (in bytes) $sz = strlen($extra); // combine the parts $data = pack('vCCvvvCCCCC', $options, 0, 1, $sz, 0, $sheetIndex, 0, 0, 0, 0, 0) . $name . $extra; $length = strlen($data); $header = pack('vv', $record, $length); return $header . $data; } /** * Stores the CODEPAGE biff record. */ private function writeCodepage(): void { $record = 0x0042; // Record identifier $length = 0x0002; // Number of bytes to follow $cv = $this->codepage; // The code page $header = pack('vv', $record, $length); $data = pack('v', $cv); $this->append($header . $data); } /** * Write Excel BIFF WINDOW1 record. */ private function writeWindow1(): void { $record = 0x003D; // Record identifier $length = 0x0012; // Number of bytes to follow $xWn = 0x0000; // Horizontal position of window $yWn = 0x0000; // Vertical position of window $dxWn = 0x25BC; // Width of window $dyWn = 0x1572; // Height of window $grbit = 0x0038; // Option flags // not supported by PhpSpreadsheet, so there is only one selected sheet, the active $ctabsel = 1; // Number of workbook tabs selected $wTabRatio = 0x0258; // Tab to scrollbar ratio // not supported by PhpSpreadsheet, set to 0 $itabFirst = 0; // 1st displayed worksheet $itabCur = $this->spreadsheet->getActiveSheetIndex(); // Active worksheet $header = pack('vv', $record, $length); $data = pack('vvvvvvvvv', $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio); $this->append($header . $data); } /** * Writes Excel BIFF BOUNDSHEET record. * * @param int $offset Location of worksheet BOF */ private function writeBoundSheet(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, $offset): void { $sheetname = $sheet->getTitle(); $record = 0x0085; // Record identifier // sheet state switch ($sheet->getSheetState()) { case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VISIBLE: $ss = 0x00; break; case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_HIDDEN: $ss = 0x01; break; case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VERYHIDDEN: $ss = 0x02; break; default: $ss = 0x00; break; } // sheet type $st = 0x00; $grbit = 0x0000; // Visibility and sheet type $data = pack('VCC', $offset, $ss, $st); $data .= StringHelper::UTF8toBIFF8UnicodeShort($sheetname); $length = strlen($data); $header = pack('vv', $record, $length); $this->append($header . $data); } /** * Write Internal SUPBOOK record. */ private function writeSupbookInternal() { $record = 0x01AE; // Record identifier $length = 0x0004; // Bytes to follow $header = pack('vv', $record, $length); $data = pack('vv', $this->spreadsheet->getSheetCount(), 0x0401); return $this->writeData($header . $data); } /** * Writes the Excel BIFF EXTERNSHEET record. These references are used by * formulas. */ private function writeExternalsheetBiff8() { $totalReferences = count($this->parser->references); $record = 0x0017; // Record identifier $length = 2 + 6 * $totalReferences; // Number of bytes to follow $supbook_index = 0; // FIXME: only using internal SUPBOOK record $header = pack('vv', $record, $length); $data = pack('v', $totalReferences); for ($i = 0; $i < $totalReferences; ++$i) { $data .= $this->parser->references[$i]; } return $this->writeData($header . $data); } /** * Write Excel BIFF STYLE records. */ private function writeStyle(): void { $record = 0x0293; // Record identifier $length = 0x0004; // Bytes to follow $ixfe = 0x8000; // Index to cell style XF $BuiltIn = 0x00; // Built-in style $iLevel = 0xff; // Outline style level $header = pack('vv', $record, $length); $data = pack('vCC', $ixfe, $BuiltIn, $iLevel); $this->append($header . $data); } /** * Writes Excel FORMAT record for non "built-in" numerical formats. * * @param string $format Custom format string * @param int $ifmt Format index code */ private function writeNumberFormat($format, $ifmt): void { $record = 0x041E; // Record identifier $numberFormatString = StringHelper::UTF8toBIFF8UnicodeLong($format); $length = 2 + strlen($numberFormatString); // Number of bytes to follow $header = pack('vv', $record, $length); $data = pack('v', $ifmt) . $numberFormatString; $this->append($header . $data); } /** * Write DATEMODE record to indicate the date system in use (1904 or 1900). */ private function writeDateMode(): void { $record = 0x0022; // Record identifier $length = 0x0002; // Bytes to follow $f1904 = (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904) ? 1 : 0; // Flag for 1904 date system $header = pack('vv', $record, $length); $data = pack('v', $f1904); $this->append($header . $data); } /** * Stores the COUNTRY record for localization. * * @return string */ private function writeCountry() { $record = 0x008C; // Record identifier $length = 4; // Number of bytes to follow $header = pack('vv', $record, $length); // using the same country code always for simplicity $data = pack('vv', $this->countryCode, $this->countryCode); return $this->writeData($header . $data); } /** * Write the RECALCID record. * * @return string */ private function writeRecalcId() { $record = 0x01C1; // Record identifier $length = 8; // Number of bytes to follow $header = pack('vv', $record, $length); // by inspection of real Excel files, MS Office Excel 2007 writes this $data = pack('VV', 0x000001C1, 0x00001E667); return $this->writeData($header . $data); } /** * Stores the PALETTE biff record. */ private function writePalette(): void { $aref = $this->palette; $record = 0x0092; // Record identifier $length = 2 + 4 * count($aref); // Number of bytes to follow $ccv = count($aref); // Number of RGB values to follow $data = ''; // The RGB data // Pack the RGB data foreach ($aref as $color) { foreach ($color as $byte) { $data .= pack('C', $byte); } } $header = pack('vvv', $record, $length, $ccv); $this->append($header . $data); } /** * Handling of the SST continue blocks is complicated by the need to include an * additional continuation byte depending on whether the string is split between * blocks or whether it starts at the beginning of the block. (There are also * additional complications that will arise later when/if Rich Strings are * supported). * * The Excel documentation says that the SST record should be followed by an * EXTSST record. The EXTSST record is a hash table that is used to optimise * access to SST. However, despite the documentation it doesn't seem to be * required so we will ignore it. * * @return string Binary data */ private function writeSharedStringsTable() { // maximum size of record data (excluding record header) $continue_limit = 8224; // initialize array of record data blocks $recordDatas = []; // start SST record data block with total number of strings, total number of unique strings $recordData = pack('VV', $this->stringTotal, $this->stringUnique); // loop through all (unique) strings in shared strings table foreach (array_keys($this->stringTable) as $string) { // here $string is a BIFF8 encoded string // length = character count $headerinfo = unpack('vlength/Cencoding', $string); // currently, this is always 1 = uncompressed $encoding = $headerinfo['encoding']; // initialize finished writing current $string $finished = false; while ($finished === false) { // normally, there will be only one cycle, but if string cannot immediately be written as is // there will be need for more than one cylcle, if string longer than one record data block, there // may be need for even more cycles if (strlen($recordData) + strlen($string) <= $continue_limit) { // then we can write the string (or remainder of string) without any problems $recordData .= $string; if (strlen($recordData) + strlen($string) == $continue_limit) { // we close the record data block, and initialize a new one $recordDatas[] = $recordData; $recordData = ''; } // we are finished writing this string $finished = true; } else { // special treatment writing the string (or remainder of the string) // If the string is very long it may need to be written in more than one CONTINUE record. // check how many bytes more there is room for in the current record $space_remaining = $continue_limit - strlen($recordData); // minimum space needed // uncompressed: 2 byte string length length field + 1 byte option flags + 2 byte character // compressed: 2 byte string length length field + 1 byte option flags + 1 byte character $min_space_needed = ($encoding == 1) ? 5 : 4; // We have two cases // 1. space remaining is less than minimum space needed // here we must waste the space remaining and move to next record data block // 2. space remaining is greater than or equal to minimum space needed // here we write as much as we can in the current block, then move to next record data block // 1. space remaining is less than minimum space needed if ($space_remaining < $min_space_needed) { // we close the block, store the block data $recordDatas[] = $recordData; // and start new record data block where we start writing the string $recordData = ''; // 2. space remaining is greater than or equal to minimum space needed } else { // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below $effective_space_remaining = $space_remaining; // for uncompressed strings, sometimes effective space remaining is reduced by 1 if ($encoding == 1 && (strlen($string) - $space_remaining) % 2 == 1) { --$effective_space_remaining; } // one block fininshed, store the block data $recordData .= substr($string, 0, $effective_space_remaining); $string = substr($string, $effective_space_remaining); // for next cycle in while loop $recordDatas[] = $recordData; // start new record data block with the repeated option flags $recordData = pack('C', $encoding); } } } } // Store the last record data block unless it is empty // if there was no need for any continue records, this will be the for SST record data block itself if (strlen($recordData) > 0) { $recordDatas[] = $recordData; } // combine into one chunk with all the blocks SST, CONTINUE,... $chunk = ''; foreach ($recordDatas as $i => $recordData) { // first block should have the SST record header, remaing should have CONTINUE header $record = ($i == 0) ? 0x00FC : 0x003C; $header = pack('vv', $record, strlen($recordData)); $data = $header . $recordData; $chunk .= $this->writeData($data); } return $chunk; } /** * Writes the MSODRAWINGGROUP record if needed. Possibly split using CONTINUE records. */ private function writeMsoDrawingGroup() { // write the Escher stream if necessary if (isset($this->escher)) { $writer = new Escher($this->escher); $data = $writer->close(); $record = 0x00EB; $length = strlen($data); $header = pack('vv', $record, $length); return $this->writeData($header . $data); } return ''; } /** * Get Escher object. */ public function getEscher(): ?\PhpOffice\PhpSpreadsheet\Shared\Escher { return $this->escher; } /** * Set Escher object. */ public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher): void { $this->escher = $escher; } } PK[. Parser.phpnu[=,;#()"{} const REGEX_SHEET_TITLE_UNQUOTED = '[^\*\:\/\\\\\?\[\]\+\-\% \\\'\^\&\<\>\=\,\;\#\(\)\"\{\}]+'; // Sheet title in quoted form (without surrounding quotes) // Invalid sheet title characters cannot occur in the sheet title: // *:/\?[] (usual invalid sheet title characters) // Single quote is represented as a pair '' const REGEX_SHEET_TITLE_QUOTED = '(([^\*\:\/\\\\\?\[\]\\\'])+|(\\\'\\\')+)+'; /** * The index of the character we are currently looking at. * * @var int */ public $currentCharacter; /** * The token we are working on. * * @var string */ public $currentToken; /** * The formula to parse. * * @var string */ private $formula; /** * The character ahead of the current char. * * @var string */ public $lookAhead; /** * The parse tree to be generated. * * @var string */ public $parseTree; /** * Array of external sheets. * * @var array */ private $externalSheets; /** * Array of sheet references in the form of REF structures. * * @var array */ public $references; /** * The Excel ptg indices. * * @var array */ private $ptg = [ 'ptgExp' => 0x01, 'ptgTbl' => 0x02, 'ptgAdd' => 0x03, 'ptgSub' => 0x04, 'ptgMul' => 0x05, 'ptgDiv' => 0x06, 'ptgPower' => 0x07, 'ptgConcat' => 0x08, 'ptgLT' => 0x09, 'ptgLE' => 0x0A, 'ptgEQ' => 0x0B, 'ptgGE' => 0x0C, 'ptgGT' => 0x0D, 'ptgNE' => 0x0E, 'ptgIsect' => 0x0F, 'ptgUnion' => 0x10, 'ptgRange' => 0x11, 'ptgUplus' => 0x12, 'ptgUminus' => 0x13, 'ptgPercent' => 0x14, 'ptgParen' => 0x15, 'ptgMissArg' => 0x16, 'ptgStr' => 0x17, 'ptgAttr' => 0x19, 'ptgSheet' => 0x1A, 'ptgEndSheet' => 0x1B, 'ptgErr' => 0x1C, 'ptgBool' => 0x1D, 'ptgInt' => 0x1E, 'ptgNum' => 0x1F, 'ptgArray' => 0x20, 'ptgFunc' => 0x21, 'ptgFuncVar' => 0x22, 'ptgName' => 0x23, 'ptgRef' => 0x24, 'ptgArea' => 0x25, 'ptgMemArea' => 0x26, 'ptgMemErr' => 0x27, 'ptgMemNoMem' => 0x28, 'ptgMemFunc' => 0x29, 'ptgRefErr' => 0x2A, 'ptgAreaErr' => 0x2B, 'ptgRefN' => 0x2C, 'ptgAreaN' => 0x2D, 'ptgMemAreaN' => 0x2E, 'ptgMemNoMemN' => 0x2F, 'ptgNameX' => 0x39, 'ptgRef3d' => 0x3A, 'ptgArea3d' => 0x3B, 'ptgRefErr3d' => 0x3C, 'ptgAreaErr3d' => 0x3D, 'ptgArrayV' => 0x40, 'ptgFuncV' => 0x41, 'ptgFuncVarV' => 0x42, 'ptgNameV' => 0x43, 'ptgRefV' => 0x44, 'ptgAreaV' => 0x45, 'ptgMemAreaV' => 0x46, 'ptgMemErrV' => 0x47, 'ptgMemNoMemV' => 0x48, 'ptgMemFuncV' => 0x49, 'ptgRefErrV' => 0x4A, 'ptgAreaErrV' => 0x4B, 'ptgRefNV' => 0x4C, 'ptgAreaNV' => 0x4D, 'ptgMemAreaNV' => 0x4E, 'ptgMemNoMemNV' => 0x4F, 'ptgFuncCEV' => 0x58, 'ptgNameXV' => 0x59, 'ptgRef3dV' => 0x5A, 'ptgArea3dV' => 0x5B, 'ptgRefErr3dV' => 0x5C, 'ptgAreaErr3dV' => 0x5D, 'ptgArrayA' => 0x60, 'ptgFuncA' => 0x61, 'ptgFuncVarA' => 0x62, 'ptgNameA' => 0x63, 'ptgRefA' => 0x64, 'ptgAreaA' => 0x65, 'ptgMemAreaA' => 0x66, 'ptgMemErrA' => 0x67, 'ptgMemNoMemA' => 0x68, 'ptgMemFuncA' => 0x69, 'ptgRefErrA' => 0x6A, 'ptgAreaErrA' => 0x6B, 'ptgRefNA' => 0x6C, 'ptgAreaNA' => 0x6D, 'ptgMemAreaNA' => 0x6E, 'ptgMemNoMemNA' => 0x6F, 'ptgFuncCEA' => 0x78, 'ptgNameXA' => 0x79, 'ptgRef3dA' => 0x7A, 'ptgArea3dA' => 0x7B, 'ptgRefErr3dA' => 0x7C, 'ptgAreaErr3dA' => 0x7D, ]; /** * Thanks to Michael Meeks and Gnumeric for the initial arg values. * * The following hash was generated by "function_locale.pl" in the distro. * Refer to function_locale.pl for non-English function names. * * The array elements are as follow: * ptg: The Excel function ptg code. * args: The number of arguments that the function takes: * >=0 is a fixed number of arguments. * -1 is a variable number of arguments. * class: The reference, value or array class of the function args. * vol: The function is volatile. * * @var array */ private $functions = [ // function ptg args class vol 'COUNT' => [0, -1, 0, 0], 'IF' => [1, -1, 1, 0], 'ISNA' => [2, 1, 1, 0], 'ISERROR' => [3, 1, 1, 0], 'SUM' => [4, -1, 0, 0], 'AVERAGE' => [5, -1, 0, 0], 'MIN' => [6, -1, 0, 0], 'MAX' => [7, -1, 0, 0], 'ROW' => [8, -1, 0, 0], 'COLUMN' => [9, -1, 0, 0], 'NA' => [10, 0, 0, 0], 'NPV' => [11, -1, 1, 0], 'STDEV' => [12, -1, 0, 0], 'DOLLAR' => [13, -1, 1, 0], 'FIXED' => [14, -1, 1, 0], 'SIN' => [15, 1, 1, 0], 'COS' => [16, 1, 1, 0], 'TAN' => [17, 1, 1, 0], 'ATAN' => [18, 1, 1, 0], 'PI' => [19, 0, 1, 0], 'SQRT' => [20, 1, 1, 0], 'EXP' => [21, 1, 1, 0], 'LN' => [22, 1, 1, 0], 'LOG10' => [23, 1, 1, 0], 'ABS' => [24, 1, 1, 0], 'INT' => [25, 1, 1, 0], 'SIGN' => [26, 1, 1, 0], 'ROUND' => [27, 2, 1, 0], 'LOOKUP' => [28, -1, 0, 0], 'INDEX' => [29, -1, 0, 1], 'REPT' => [30, 2, 1, 0], 'MID' => [31, 3, 1, 0], 'LEN' => [32, 1, 1, 0], 'VALUE' => [33, 1, 1, 0], 'TRUE' => [34, 0, 1, 0], 'FALSE' => [35, 0, 1, 0], 'AND' => [36, -1, 0, 0], 'OR' => [37, -1, 0, 0], 'NOT' => [38, 1, 1, 0], 'MOD' => [39, 2, 1, 0], 'DCOUNT' => [40, 3, 0, 0], 'DSUM' => [41, 3, 0, 0], 'DAVERAGE' => [42, 3, 0, 0], 'DMIN' => [43, 3, 0, 0], 'DMAX' => [44, 3, 0, 0], 'DSTDEV' => [45, 3, 0, 0], 'VAR' => [46, -1, 0, 0], 'DVAR' => [47, 3, 0, 0], 'TEXT' => [48, 2, 1, 0], 'LINEST' => [49, -1, 0, 0], 'TREND' => [50, -1, 0, 0], 'LOGEST' => [51, -1, 0, 0], 'GROWTH' => [52, -1, 0, 0], 'PV' => [56, -1, 1, 0], 'FV' => [57, -1, 1, 0], 'NPER' => [58, -1, 1, 0], 'PMT' => [59, -1, 1, 0], 'RATE' => [60, -1, 1, 0], 'MIRR' => [61, 3, 0, 0], 'IRR' => [62, -1, 0, 0], 'RAND' => [63, 0, 1, 1], 'MATCH' => [64, -1, 0, 0], 'DATE' => [65, 3, 1, 0], 'TIME' => [66, 3, 1, 0], 'DAY' => [67, 1, 1, 0], 'MONTH' => [68, 1, 1, 0], 'YEAR' => [69, 1, 1, 0], 'WEEKDAY' => [70, -1, 1, 0], 'HOUR' => [71, 1, 1, 0], 'MINUTE' => [72, 1, 1, 0], 'SECOND' => [73, 1, 1, 0], 'NOW' => [74, 0, 1, 1], 'AREAS' => [75, 1, 0, 1], 'ROWS' => [76, 1, 0, 1], 'COLUMNS' => [77, 1, 0, 1], 'OFFSET' => [78, -1, 0, 1], 'SEARCH' => [82, -1, 1, 0], 'TRANSPOSE' => [83, 1, 1, 0], 'TYPE' => [86, 1, 1, 0], 'ATAN2' => [97, 2, 1, 0], 'ASIN' => [98, 1, 1, 0], 'ACOS' => [99, 1, 1, 0], 'CHOOSE' => [100, -1, 1, 0], 'HLOOKUP' => [101, -1, 0, 0], 'VLOOKUP' => [102, -1, 0, 0], 'ISREF' => [105, 1, 0, 0], 'LOG' => [109, -1, 1, 0], 'CHAR' => [111, 1, 1, 0], 'LOWER' => [112, 1, 1, 0], 'UPPER' => [113, 1, 1, 0], 'PROPER' => [114, 1, 1, 0], 'LEFT' => [115, -1, 1, 0], 'RIGHT' => [116, -1, 1, 0], 'EXACT' => [117, 2, 1, 0], 'TRIM' => [118, 1, 1, 0], 'REPLACE' => [119, 4, 1, 0], 'SUBSTITUTE' => [120, -1, 1, 0], 'CODE' => [121, 1, 1, 0], 'FIND' => [124, -1, 1, 0], 'CELL' => [125, -1, 0, 1], 'ISERR' => [126, 1, 1, 0], 'ISTEXT' => [127, 1, 1, 0], 'ISNUMBER' => [128, 1, 1, 0], 'ISBLANK' => [129, 1, 1, 0], 'T' => [130, 1, 0, 0], 'N' => [131, 1, 0, 0], 'DATEVALUE' => [140, 1, 1, 0], 'TIMEVALUE' => [141, 1, 1, 0], 'SLN' => [142, 3, 1, 0], 'SYD' => [143, 4, 1, 0], 'DDB' => [144, -1, 1, 0], 'INDIRECT' => [148, -1, 1, 1], 'CALL' => [150, -1, 1, 0], 'CLEAN' => [162, 1, 1, 0], 'MDETERM' => [163, 1, 2, 0], 'MINVERSE' => [164, 1, 2, 0], 'MMULT' => [165, 2, 2, 0], 'IPMT' => [167, -1, 1, 0], 'PPMT' => [168, -1, 1, 0], 'COUNTA' => [169, -1, 0, 0], 'PRODUCT' => [183, -1, 0, 0], 'FACT' => [184, 1, 1, 0], 'DPRODUCT' => [189, 3, 0, 0], 'ISNONTEXT' => [190, 1, 1, 0], 'STDEVP' => [193, -1, 0, 0], 'VARP' => [194, -1, 0, 0], 'DSTDEVP' => [195, 3, 0, 0], 'DVARP' => [196, 3, 0, 0], 'TRUNC' => [197, -1, 1, 0], 'ISLOGICAL' => [198, 1, 1, 0], 'DCOUNTA' => [199, 3, 0, 0], 'USDOLLAR' => [204, -1, 1, 0], 'FINDB' => [205, -1, 1, 0], 'SEARCHB' => [206, -1, 1, 0], 'REPLACEB' => [207, 4, 1, 0], 'LEFTB' => [208, -1, 1, 0], 'RIGHTB' => [209, -1, 1, 0], 'MIDB' => [210, 3, 1, 0], 'LENB' => [211, 1, 1, 0], 'ROUNDUP' => [212, 2, 1, 0], 'ROUNDDOWN' => [213, 2, 1, 0], 'ASC' => [214, 1, 1, 0], 'DBCS' => [215, 1, 1, 0], 'RANK' => [216, -1, 0, 0], 'ADDRESS' => [219, -1, 1, 0], 'DAYS360' => [220, -1, 1, 0], 'TODAY' => [221, 0, 1, 1], 'VDB' => [222, -1, 1, 0], 'MEDIAN' => [227, -1, 0, 0], 'SUMPRODUCT' => [228, -1, 2, 0], 'SINH' => [229, 1, 1, 0], 'COSH' => [230, 1, 1, 0], 'TANH' => [231, 1, 1, 0], 'ASINH' => [232, 1, 1, 0], 'ACOSH' => [233, 1, 1, 0], 'ATANH' => [234, 1, 1, 0], 'DGET' => [235, 3, 0, 0], 'INFO' => [244, 1, 1, 1], 'DB' => [247, -1, 1, 0], 'FREQUENCY' => [252, 2, 0, 0], 'ERROR.TYPE' => [261, 1, 1, 0], 'REGISTER.ID' => [267, -1, 1, 0], 'AVEDEV' => [269, -1, 0, 0], 'BETADIST' => [270, -1, 1, 0], 'GAMMALN' => [271, 1, 1, 0], 'BETAINV' => [272, -1, 1, 0], 'BINOMDIST' => [273, 4, 1, 0], 'CHIDIST' => [274, 2, 1, 0], 'CHIINV' => [275, 2, 1, 0], 'COMBIN' => [276, 2, 1, 0], 'CONFIDENCE' => [277, 3, 1, 0], 'CRITBINOM' => [278, 3, 1, 0], 'EVEN' => [279, 1, 1, 0], 'EXPONDIST' => [280, 3, 1, 0], 'FDIST' => [281, 3, 1, 0], 'FINV' => [282, 3, 1, 0], 'FISHER' => [283, 1, 1, 0], 'FISHERINV' => [284, 1, 1, 0], 'FLOOR' => [285, 2, 1, 0], 'GAMMADIST' => [286, 4, 1, 0], 'GAMMAINV' => [287, 3, 1, 0], 'CEILING' => [288, 2, 1, 0], 'HYPGEOMDIST' => [289, 4, 1, 0], 'LOGNORMDIST' => [290, 3, 1, 0], 'LOGINV' => [291, 3, 1, 0], 'NEGBINOMDIST' => [292, 3, 1, 0], 'NORMDIST' => [293, 4, 1, 0], 'NORMSDIST' => [294, 1, 1, 0], 'NORMINV' => [295, 3, 1, 0], 'NORMSINV' => [296, 1, 1, 0], 'STANDARDIZE' => [297, 3, 1, 0], 'ODD' => [298, 1, 1, 0], 'PERMUT' => [299, 2, 1, 0], 'POISSON' => [300, 3, 1, 0], 'TDIST' => [301, 3, 1, 0], 'WEIBULL' => [302, 4, 1, 0], 'SUMXMY2' => [303, 2, 2, 0], 'SUMX2MY2' => [304, 2, 2, 0], 'SUMX2PY2' => [305, 2, 2, 0], 'CHITEST' => [306, 2, 2, 0], 'CORREL' => [307, 2, 2, 0], 'COVAR' => [308, 2, 2, 0], 'FORECAST' => [309, 3, 2, 0], 'FTEST' => [310, 2, 2, 0], 'INTERCEPT' => [311, 2, 2, 0], 'PEARSON' => [312, 2, 2, 0], 'RSQ' => [313, 2, 2, 0], 'STEYX' => [314, 2, 2, 0], 'SLOPE' => [315, 2, 2, 0], 'TTEST' => [316, 4, 2, 0], 'PROB' => [317, -1, 2, 0], 'DEVSQ' => [318, -1, 0, 0], 'GEOMEAN' => [319, -1, 0, 0], 'HARMEAN' => [320, -1, 0, 0], 'SUMSQ' => [321, -1, 0, 0], 'KURT' => [322, -1, 0, 0], 'SKEW' => [323, -1, 0, 0], 'ZTEST' => [324, -1, 0, 0], 'LARGE' => [325, 2, 0, 0], 'SMALL' => [326, 2, 0, 0], 'QUARTILE' => [327, 2, 0, 0], 'PERCENTILE' => [328, 2, 0, 0], 'PERCENTRANK' => [329, -1, 0, 0], 'MODE' => [330, -1, 2, 0], 'TRIMMEAN' => [331, 2, 0, 0], 'TINV' => [332, 2, 1, 0], 'CONCATENATE' => [336, -1, 1, 0], 'POWER' => [337, 2, 1, 0], 'RADIANS' => [342, 1, 1, 0], 'DEGREES' => [343, 1, 1, 0], 'SUBTOTAL' => [344, -1, 0, 0], 'SUMIF' => [345, -1, 0, 0], 'COUNTIF' => [346, 2, 0, 0], 'COUNTBLANK' => [347, 1, 0, 0], 'ISPMT' => [350, 4, 1, 0], 'DATEDIF' => [351, 3, 1, 0], 'DATESTRING' => [352, 1, 1, 0], 'NUMBERSTRING' => [353, 2, 1, 0], 'ROMAN' => [354, -1, 1, 0], 'GETPIVOTDATA' => [358, -1, 0, 0], 'HYPERLINK' => [359, -1, 1, 0], 'PHONETIC' => [360, 1, 0, 0], 'AVERAGEA' => [361, -1, 0, 0], 'MAXA' => [362, -1, 0, 0], 'MINA' => [363, -1, 0, 0], 'STDEVPA' => [364, -1, 0, 0], 'VARPA' => [365, -1, 0, 0], 'STDEVA' => [366, -1, 0, 0], 'VARA' => [367, -1, 0, 0], 'BAHTTEXT' => [368, 1, 0, 0], ]; private $spreadsheet; /** * The class constructor. */ public function __construct(Spreadsheet $spreadsheet) { $this->spreadsheet = $spreadsheet; $this->currentCharacter = 0; $this->currentToken = ''; // The token we are working on. $this->formula = ''; // The formula to parse. $this->lookAhead = ''; // The character ahead of the current char. $this->parseTree = ''; // The parse tree to be generated. $this->externalSheets = []; $this->references = []; } /** * Convert a token to the proper ptg value. * * @param mixed $token the token to convert * * @return mixed the converted token on success */ private function convert($token) { if (preg_match('/"([^"]|""){0,255}"/', $token)) { return $this->convertString($token); } elseif (is_numeric($token)) { return $this->convertNumber($token); // match references like A1 or $A$1 } elseif (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) { return $this->convertRef2d($token); // match external references like Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1 } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) { return $this->convertRef3d($token); // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1 } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\\d+)$/u", $token)) { return $this->convertRef3d($token); // match ranges like A1:B2 or $A$1:$B$2 } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) { return $this->convertRange2d($token); // match external ranges like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)\\:\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)$/u', $token)) { return $this->convertRange3d($token); // match external ranges like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)\\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)$/u", $token)) { return $this->convertRange3d($token); // operators (including parentheses) } elseif (isset($this->ptg[$token])) { return pack('C', $this->ptg[$token]); // match error codes } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') { return $this->convertError($token); } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) { return $this->convertDefinedName($token); // commented so argument number can be processed correctly. See toReversePolish(). /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token)) { return($this->convertFunction($token, $this->_func_args)); }*/ // if it's an argument, ignore the token (the argument remains) } elseif ($token == 'arg') { return ''; } // TODO: use real error codes throw new WriterException("Unknown token $token"); } /** * Convert a number token to ptgInt or ptgNum. * * @param mixed $num an integer or double for conversion to its ptg value * * @return string */ private function convertNumber($num) { // Integer in the range 0..2**16-1 if ((preg_match('/^\\d+$/', $num)) && ($num <= 65535)) { return pack('Cv', $this->ptg['ptgInt'], $num); } // A float if (BIFFwriter::getByteOrder()) { // if it's Big Endian $num = strrev($num); } return pack('Cd', $this->ptg['ptgNum'], $num); } /** * Convert a string token to ptgStr. * * @param string $string a string for conversion to its ptg value * * @return mixed the converted token on success */ private function convertString($string) { // chop away beggining and ending quotes $string = substr($string, 1, -1); if (strlen($string) > 255) { throw new WriterException('String is too long'); } return pack('C', $this->ptg['ptgStr']) . StringHelper::UTF8toBIFF8UnicodeShort($string); } /** * Convert a function to a ptgFunc or ptgFuncVarV depending on the number of * args that it takes. * * @param string $token the name of the function for convertion to ptg value * @param int $num_args the number of arguments the function receives * * @return string The packed ptg for the function */ private function convertFunction($token, $num_args) { $args = $this->functions[$token][1]; // Fixed number of args eg. TIME($i, $j, $k). if ($args >= 0) { return pack('Cv', $this->ptg['ptgFuncV'], $this->functions[$token][0]); } // Variable number of args eg. SUM($i, $j, $k, ..). return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]); } /** * Convert an Excel range such as A1:D4 to a ptgRefV. * * @param string $range An Excel range in the A1:A2 * @param int $class * * @return string */ private function convertRange2d($range, $class = 0) { // TODO: possible class value 0,1,2 check Formula.pm // Split the range into 2 cell refs if (preg_match('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) { [$cell1, $cell2] = explode(':', $range); } else { // TODO: use real error codes throw new WriterException('Unknown range separator'); } // Convert the cell references [$row1, $col1] = $this->cellToPackedRowcol($cell1); [$row2, $col2] = $this->cellToPackedRowcol($cell2); // The ptg value depends on the class of the ptg. if ($class == 0) { $ptgArea = pack('C', $this->ptg['ptgArea']); } elseif ($class == 1) { $ptgArea = pack('C', $this->ptg['ptgAreaV']); } elseif ($class == 2) { $ptgArea = pack('C', $this->ptg['ptgAreaA']); } else { // TODO: use real error codes throw new WriterException("Unknown class $class"); } return $ptgArea . $row1 . $row2 . $col1 . $col2; } /** * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to * a ptgArea3d. * * @param string $token an Excel range in the Sheet1!A1:A2 format * * @return mixed the packed ptgArea3d token on success */ private function convertRange3d($token) { // Split the ref at the ! symbol [$ext_ref, $range] = PhpspreadsheetWorksheet::extractSheetTitle($token, true); // Convert the external reference part (different for BIFF8) $ext_ref = $this->getRefIndex($ext_ref); // Split the range into 2 cell refs [$cell1, $cell2] = explode(':', $range); // Convert the cell references if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\\d+)$/', $cell1)) { [$row1, $col1] = $this->cellToPackedRowcol($cell1); [$row2, $col2] = $this->cellToPackedRowcol($cell2); } else { // It's a rows range (like 26:27) [$row1, $col1, $row2, $col2] = $this->rangeToPackedRange($cell1 . ':' . $cell2); } // The ptg value depends on the class of the ptg. $ptgArea = pack('C', $this->ptg['ptgArea3d']); return $ptgArea . $ext_ref . $row1 . $row2 . $col1 . $col2; } /** * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV. * * @param string $cell An Excel cell reference * * @return string The cell in packed() format with the corresponding ptg */ private function convertRef2d($cell) { // Convert the cell reference $cell_array = $this->cellToPackedRowcol($cell); [$row, $col] = $cell_array; // The ptg value depends on the class of the ptg. $ptgRef = pack('C', $this->ptg['ptgRefA']); return $ptgRef . $row . $col; } /** * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a * ptgRef3d. * * @param string $cell An Excel cell reference * * @return mixed the packed ptgRef3d token on success */ private function convertRef3d($cell) { // Split the ref at the ! symbol [$ext_ref, $cell] = PhpspreadsheetWorksheet::extractSheetTitle($cell, true); // Convert the external reference part (different for BIFF8) $ext_ref = $this->getRefIndex($ext_ref); // Convert the cell reference part [$row, $col] = $this->cellToPackedRowcol($cell); // The ptg value depends on the class of the ptg. $ptgRef = pack('C', $this->ptg['ptgRef3dA']); return $ptgRef . $ext_ref . $row . $col; } /** * Convert an error code to a ptgErr. * * @param string $errorCode The error code for conversion to its ptg value * * @return string The error code ptgErr */ private function convertError($errorCode) { switch ($errorCode) { case '#NULL!': return pack('C', 0x00); case '#DIV/0!': return pack('C', 0x07); case '#VALUE!': return pack('C', 0x0F); case '#REF!': return pack('C', 0x17); case '#NAME?': return pack('C', 0x1D); case '#NUM!': return pack('C', 0x24); case '#N/A': return pack('C', 0x2A); } return pack('C', 0xFF); } private function convertDefinedName(string $name): string { if (strlen($name) > 255) { throw new WriterException('Defined Name is too long'); } $nameReference = 1; foreach ($this->spreadsheet->getDefinedNames() as $definedName) { if ($name === $definedName->getName()) { break; } ++$nameReference; } $ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference); throw new WriterException('Cannot yet write formulae with defined names to Xls'); return $ptgRef; } /** * Look up the REF index that corresponds to an external sheet name * (or range). If it doesn't exist yet add it to the workbook's references * array. It assumes all sheet names given must exist. * * @param string $ext_ref The name of the external reference * * @return mixed The reference index in packed() format on success */ private function getRefIndex($ext_ref) { $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading ' if any. $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any. $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with ' // Check if there is a sheet range eg., Sheet1:Sheet2. if (preg_match('/:/', $ext_ref)) { [$sheet_name1, $sheet_name2] = explode(':', $ext_ref); $sheet1 = $this->getSheetIndex($sheet_name1); if ($sheet1 == -1) { throw new WriterException("Unknown sheet name $sheet_name1 in formula"); } $sheet2 = $this->getSheetIndex($sheet_name2); if ($sheet2 == -1) { throw new WriterException("Unknown sheet name $sheet_name2 in formula"); } // Reverse max and min sheet numbers if necessary if ($sheet1 > $sheet2) { [$sheet1, $sheet2] = [$sheet2, $sheet1]; } } else { // Single sheet name only. $sheet1 = $this->getSheetIndex($ext_ref); if ($sheet1 == -1) { throw new WriterException("Unknown sheet name $ext_ref in formula"); } $sheet2 = $sheet1; } // assume all references belong to this document $supbook_index = 0x00; $ref = pack('vvv', $supbook_index, $sheet1, $sheet2); $totalreferences = count($this->references); $index = -1; for ($i = 0; $i < $totalreferences; ++$i) { if ($ref == $this->references[$i]) { $index = $i; break; } } // if REF was not found add it to references array if ($index == -1) { $this->references[$totalreferences] = $ref; $index = $totalreferences; } return pack('v', $index); } /** * Look up the index that corresponds to an external sheet name. The hash of * sheet names is updated by the addworksheet() method of the * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. * * @param string $sheet_name Sheet name * * @return int The sheet index, -1 if the sheet was not found */ private function getSheetIndex($sheet_name) { if (!isset($this->externalSheets[$sheet_name])) { return -1; } return $this->externalSheets[$sheet_name]; } /** * This method is used to update the array of sheet names. It is * called by the addWorksheet() method of the * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. * * @param string $name The name of the worksheet being added * @param int $index The index of the worksheet being added * * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::addWorksheet() */ public function setExtSheet($name, $index): void { $this->externalSheets[$name] = $index; } /** * pack() row and column into the required 3 or 4 byte format. * * @param string $cell The Excel cell reference to be packed * * @return array Array containing the row and column in packed() format */ private function cellToPackedRowcol($cell) { $cell = strtoupper($cell); [$row, $col, $row_rel, $col_rel] = $this->cellToRowcol($cell); if ($col >= 256) { throw new WriterException("Column in: $cell greater than 255"); } if ($row >= 65536) { throw new WriterException("Row in: $cell greater than 65536 "); } // Set the high bits to indicate if row or col are relative. $col |= $col_rel << 14; $col |= $row_rel << 15; $col = pack('v', $col); $row = pack('v', $row); return [$row, $col]; } /** * pack() row range into the required 3 or 4 byte format. * Just using maximum col/rows, which is probably not the correct solution. * * @param string $range The Excel range to be packed * * @return array Array containing (row1,col1,row2,col2) in packed() format */ private function rangeToPackedRange($range) { preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match); // return absolute rows if there is a $ in the ref $row1_rel = empty($match[1]) ? 1 : 0; $row1 = $match[2]; $row2_rel = empty($match[3]) ? 1 : 0; $row2 = $match[4]; // Convert 1-index to zero-index --$row1; --$row2; // Trick poor inocent Excel $col1 = 0; $col2 = 65535; // FIXME: maximum possible value for Excel 5 (change this!!!) // FIXME: this changes for BIFF8 if (($row1 >= 65536) || ($row2 >= 65536)) { throw new WriterException("Row in: $range greater than 65536 "); } // Set the high bits to indicate if rows are relative. $col1 |= $row1_rel << 15; $col2 |= $row2_rel << 15; $col1 = pack('v', $col1); $col2 = pack('v', $col2); $row1 = pack('v', $row1); $row2 = pack('v', $row2); return [$row1, $col1, $row2, $col2]; } /** * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero * indexed row and column number. Also returns two (0,1) values to indicate * whether the row or column are relative references. * * @param string $cell the Excel cell reference in A1 format * * @return array */ private function cellToRowcol($cell) { preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match); // return absolute column if there is a $ in the ref $col_rel = empty($match[1]) ? 1 : 0; $col_ref = $match[2]; $row_rel = empty($match[3]) ? 1 : 0; $row = $match[4]; // Convert base26 column string to a number. $expn = strlen($col_ref) - 1; $col = 0; $col_ref_length = strlen($col_ref); for ($i = 0; $i < $col_ref_length; ++$i) { $col += (ord($col_ref[$i]) - 64) * 26 ** $expn; --$expn; } // Convert 1-index to zero-index --$row; --$col; return [$row, $col, $row_rel, $col_rel]; } /** * Advance to the next valid token. */ private function advance() { $token = ''; $i = $this->currentCharacter; $formula_length = strlen($this->formula); // eat up white spaces if ($i < $formula_length) { while ($this->formula[$i] == ' ') { ++$i; } if ($i < ($formula_length - 1)) { $this->lookAhead = $this->formula[$i + 1]; } $token = ''; } while ($i < $formula_length) { $token .= $this->formula[$i]; if ($i < ($formula_length - 1)) { $this->lookAhead = $this->formula[$i + 1]; } else { $this->lookAhead = ''; } if ($this->match($token) != '') { $this->currentCharacter = $i + 1; $this->currentToken = $token; return 1; } if ($i < ($formula_length - 2)) { $this->lookAhead = $this->formula[$i + 2]; } else { // if we run out of characters lookAhead becomes empty $this->lookAhead = ''; } ++$i; } //die("Lexical error ".$this->currentCharacter); } /** * Checks if it's a valid token. * * @param mixed $token the token to check * * @return mixed The checked token or false on failure */ private function match($token) { switch ($token) { case '+': case '-': case '*': case '/': case '(': case ')': case ',': case ';': case '>=': case '<=': case '=': case '<>': case '^': case '&': case '%': return $token; break; case '>': if ($this->lookAhead === '=') { // it's a GE token break; } return $token; break; case '<': // it's a LE or a NE token if (($this->lookAhead === '=') || ($this->lookAhead === '>')) { break; } return $token; break; default: // if it's a reference A1 or $A$1 or $A1 or A$1 if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') && ($this->lookAhead !== '!')) { return $token; } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) { // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) return $token; } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) { // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) return $token; } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) { // if it's a range A1:A2 or $A$1:$A$2 return $token; } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead)) { // If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 return $token; } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead)) { // If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 return $token; } elseif (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) { // If it's a number (check that it's not a sheet name or range) return $token; } elseif (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) { // If it's a string (of maximum 255 characters) return $token; } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token === '#N/A') { // If it's an error code return $token; } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) && ($this->lookAhead === '(')) { // if it's a function call return $token; } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token) && $this->spreadsheet->getDefinedName($token) !== null) { return $token; } elseif (substr($token, -1) === ')') { // It's an argument of some description (e.g. a named range), // precise nature yet to be determined return $token; } return ''; } } /** * The parsing method. It parses a formula. * * @param string $formula the formula to parse, without the initial equal * sign (=) * * @return mixed true on success */ public function parse($formula) { $this->currentCharacter = 0; $this->formula = (string) $formula; $this->lookAhead = $formula[1] ?? ''; $this->advance(); $this->parseTree = $this->condition(); return true; } /** * It parses a condition. It assumes the following rule: * Cond -> Expr [(">" | "<") Expr]. * * @return mixed The parsed ptg'd tree on success */ private function condition() { $result = $this->expression(); if ($this->currentToken == '<') { $this->advance(); $result2 = $this->expression(); $result = $this->createTree('ptgLT', $result, $result2); } elseif ($this->currentToken == '>') { $this->advance(); $result2 = $this->expression(); $result = $this->createTree('ptgGT', $result, $result2); } elseif ($this->currentToken == '<=') { $this->advance(); $result2 = $this->expression(); $result = $this->createTree('ptgLE', $result, $result2); } elseif ($this->currentToken == '>=') { $this->advance(); $result2 = $this->expression(); $result = $this->createTree('ptgGE', $result, $result2); } elseif ($this->currentToken == '=') { $this->advance(); $result2 = $this->expression(); $result = $this->createTree('ptgEQ', $result, $result2); } elseif ($this->currentToken == '<>') { $this->advance(); $result2 = $this->expression(); $result = $this->createTree('ptgNE', $result, $result2); } return $result; } /** * It parses a expression. It assumes the following rule: * Expr -> Term [("+" | "-") Term] * -> "string" * -> "-" Term : Negative value * -> "+" Term : Positive value * -> Error code. * * @return mixed The parsed ptg'd tree on success */ private function expression() { // If it's a string return a string node if (preg_match('/"([^"]|""){0,255}"/', $this->currentToken)) { $tmp = str_replace('""', '"', $this->currentToken); if (($tmp == '"') || ($tmp == '')) { // Trap for "" that has been used for an empty string $tmp = '""'; } $result = $this->createTree($tmp, '', ''); $this->advance(); return $result; // If it's an error code } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken) || $this->currentToken == '#N/A') { $result = $this->createTree($this->currentToken, 'ptgErr', ''); $this->advance(); return $result; // If it's a negative value } elseif ($this->currentToken == '-') { // catch "-" Term $this->advance(); $result2 = $this->expression(); return $this->createTree('ptgUminus', $result2, ''); // If it's a positive value } elseif ($this->currentToken == '+') { // catch "+" Term $this->advance(); $result2 = $this->expression(); return $this->createTree('ptgUplus', $result2, ''); } $result = $this->term(); while ($this->currentToken === '&') { $this->advance(); $result2 = $this->expression(); $result = $this->createTree('ptgConcat', $result, $result2); } while ( ($this->currentToken == '+') || ($this->currentToken == '-') || ($this->currentToken == '^') ) { if ($this->currentToken == '+') { $this->advance(); $result2 = $this->term(); $result = $this->createTree('ptgAdd', $result, $result2); } elseif ($this->currentToken == '-') { $this->advance(); $result2 = $this->term(); $result = $this->createTree('ptgSub', $result, $result2); } else { $this->advance(); $result2 = $this->term(); $result = $this->createTree('ptgPower', $result, $result2); } } return $result; } /** * This function just introduces a ptgParen element in the tree, so that Excel * doesn't get confused when working with a parenthesized formula afterwards. * * @return array The parsed ptg'd tree * * @see fact() */ private function parenthesizedExpression() { return $this->createTree('ptgParen', $this->expression(), ''); } /** * It parses a term. It assumes the following rule: * Term -> Fact [("*" | "/") Fact]. * * @return mixed The parsed ptg'd tree on success */ private function term() { $result = $this->fact(); while ( ($this->currentToken == '*') || ($this->currentToken == '/') ) { if ($this->currentToken == '*') { $this->advance(); $result2 = $this->fact(); $result = $this->createTree('ptgMul', $result, $result2); } else { $this->advance(); $result2 = $this->fact(); $result = $this->createTree('ptgDiv', $result, $result2); } } return $result; } /** * It parses a factor. It assumes the following rule: * Fact -> ( Expr ) * | CellRef * | CellRange * | Number * | Function. * * @return mixed The parsed ptg'd tree on success */ private function fact() { if ($this->currentToken === '(') { $this->advance(); // eat the "(" $result = $this->parenthesizedExpression(); if ($this->currentToken !== ')') { throw new WriterException("')' token expected."); } $this->advance(); // eat the ")" return $result; } // if it's a reference if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) { $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $this->currentToken)) { // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $this->currentToken)) { // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; } elseif ( preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) || preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) ) { // if it's a range A1:B2 or $A$1:$B$2 // must be an error? $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $this->currentToken)) { // If it's an external range (Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2) // must be an error? $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $this->currentToken)) { // If it's an external range ('Sheet1'!A1:B2 or 'Sheet1'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1'!$A$1:$B$2) // must be an error? $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; } elseif (is_numeric($this->currentToken)) { // If it's a number or a percent if ($this->lookAhead === '%') { $result = $this->createTree('ptgPercent', $this->currentToken, ''); $this->advance(); // Skip the percentage operator once we've pre-built that tree } else { $result = $this->createTree($this->currentToken, '', ''); } $this->advance(); return $result; } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) && ($this->lookAhead === '(')) { // if it's a function call return $this->func(); } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $this->currentToken) && $this->spreadsheet->getDefinedName($this->currentToken) !== null) { $result = $this->createTree('ptgName', $this->currentToken, ''); $this->advance(); return $result; } throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter); } /** * It parses a function call. It assumes the following rule: * Func -> ( Expr [,Expr]* ). * * @return mixed The parsed ptg'd tree on success */ private function func() { $num_args = 0; // number of arguments received $function = strtoupper($this->currentToken); $result = ''; // initialize result $this->advance(); $this->advance(); // eat the "(" while ($this->currentToken !== ')') { if ($num_args > 0) { if ($this->currentToken === ',' || $this->currentToken === ';') { $this->advance(); // eat the "," or ";" } else { throw new WriterException("Syntax error: comma expected in function $function, arg #{$num_args}"); } $result2 = $this->condition(); $result = $this->createTree('arg', $result, $result2); } else { // first argument $result2 = $this->condition(); $result = $this->createTree('arg', '', $result2); } ++$num_args; } if (!isset($this->functions[$function])) { throw new WriterException("Function $function() doesn't exist"); } $args = $this->functions[$function][1]; // If fixed number of args eg. TIME($i, $j, $k). Check that the number of args is valid. if (($args >= 0) && ($args != $num_args)) { throw new WriterException("Incorrect number of arguments in function $function() "); } $result = $this->createTree($function, $result, $num_args); $this->advance(); // eat the ")" return $result; } /** * Creates a tree. In fact an array which may have one or two arrays (sub-trees) * as elements. * * @param mixed $value the value of this node * @param mixed $left the left array (sub-tree) or a final node * @param mixed $right the right array (sub-tree) or a final node * * @return array A tree */ private function createTree($value, $left, $right) { return ['value' => $value, 'left' => $left, 'right' => $right]; } /** * Builds a string containing the tree in reverse polish notation (What you * would use in a HP calculator stack). * The following tree:. * * + * / \ * 2 3 * * produces: "23+" * * The following tree: * * + * / \ * 3 * * / \ * 6 A1 * * produces: "36A1*+" * * In fact all operands, functions, references, etc... are written as ptg's * * @param array $tree the optional tree to convert * * @return string The tree in reverse polish notation */ public function toReversePolish($tree = []) { $polish = ''; // the string we are going to return if (empty($tree)) { // If it's the first call use parseTree $tree = $this->parseTree; } if (is_array($tree['left'])) { $converted_tree = $this->toReversePolish($tree['left']); $polish .= $converted_tree; } elseif ($tree['left'] != '') { // It's a final node $converted_tree = $this->convert($tree['left']); $polish .= $converted_tree; } if (is_array($tree['right'])) { $converted_tree = $this->toReversePolish($tree['right']); $polish .= $converted_tree; } elseif ($tree['right'] != '') { // It's a final node $converted_tree = $this->convert($tree['right']); $polish .= $converted_tree; } // if it's a function convert it here (so we can set it's arguments) if ( preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/", $tree['value']) && !preg_match('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $tree['value']) && !preg_match('/^[A-Ia-i]?[A-Za-z](\\d+)\\.\\.[A-Ia-i]?[A-Za-z](\\d+)$/', $tree['value']) && !is_numeric($tree['value']) && !isset($this->ptg[$tree['value']]) ) { // left subtree for a function is always an array. if ($tree['left'] != '') { $left_tree = $this->toReversePolish($tree['left']); } else { $left_tree = ''; } // add it's left subtree and return. return $left_tree . $this->convertFunction($tree['value'], $tree['right']); } $converted_tree = $this->convert($tree['value']); return $polish . $converted_tree; } } PK[a!eStyle/CellFill.phpnu[ */ protected static $fillStyleMap = [ Fill::FILL_NONE => 0x00, Fill::FILL_SOLID => 0x01, Fill::FILL_PATTERN_MEDIUMGRAY => 0x02, Fill::FILL_PATTERN_DARKGRAY => 0x03, Fill::FILL_PATTERN_LIGHTGRAY => 0x04, Fill::FILL_PATTERN_DARKHORIZONTAL => 0x05, Fill::FILL_PATTERN_DARKVERTICAL => 0x06, Fill::FILL_PATTERN_DARKDOWN => 0x07, Fill::FILL_PATTERN_DARKUP => 0x08, Fill::FILL_PATTERN_DARKGRID => 0x09, Fill::FILL_PATTERN_DARKTRELLIS => 0x0A, Fill::FILL_PATTERN_LIGHTHORIZONTAL => 0x0B, Fill::FILL_PATTERN_LIGHTVERTICAL => 0x0C, Fill::FILL_PATTERN_LIGHTDOWN => 0x0D, Fill::FILL_PATTERN_LIGHTUP => 0x0E, Fill::FILL_PATTERN_LIGHTGRID => 0x0F, Fill::FILL_PATTERN_LIGHTTRELLIS => 0x10, Fill::FILL_PATTERN_GRAY125 => 0x11, Fill::FILL_PATTERN_GRAY0625 => 0x12, Fill::FILL_GRADIENT_LINEAR => 0x00, // does not exist in BIFF8 Fill::FILL_GRADIENT_PATH => 0x00, // does not exist in BIFF8 ]; public static function style(Fill $fill): int { $fillStyle = $fill->getFillType(); if (is_string($fillStyle) && array_key_exists($fillStyle, self::$fillStyleMap)) { return self::$fillStyleMap[$fillStyle]; } return self::$fillStyleMap[Fill::FILL_NONE]; } } PK[* CVVStyle/CellBorder.phpnu[ */ protected static $styleMap = [ Border::BORDER_NONE => 0x00, Border::BORDER_THIN => 0x01, Border::BORDER_MEDIUM => 0x02, Border::BORDER_DASHED => 0x03, Border::BORDER_DOTTED => 0x04, Border::BORDER_THICK => 0x05, Border::BORDER_DOUBLE => 0x06, Border::BORDER_HAIR => 0x07, Border::BORDER_MEDIUMDASHED => 0x08, Border::BORDER_DASHDOT => 0x09, Border::BORDER_MEDIUMDASHDOT => 0x0A, Border::BORDER_DASHDOTDOT => 0x0B, Border::BORDER_MEDIUMDASHDOTDOT => 0x0C, Border::BORDER_SLANTDASHDOT => 0x0D, ]; public static function style(Border $border): int { $borderStyle = $border->getBorderStyle(); if (is_string($borderStyle) && array_key_exists($borderStyle, self::$styleMap)) { return self::$styleMap[$borderStyle]; } return self::$styleMap[Border::BORDER_NONE]; } } PK[o% % Style/ColorMap.phpnu[ */ private static $colorMap = [ '#000000' => 0x08, '#FFFFFF' => 0x09, '#FF0000' => 0x0A, '#00FF00' => 0x0B, '#0000FF' => 0x0C, '#FFFF00' => 0x0D, '#FF00FF' => 0x0E, '#00FFFF' => 0x0F, '#800000' => 0x10, '#008000' => 0x11, '#000080' => 0x12, '#808000' => 0x13, '#800080' => 0x14, '#008080' => 0x15, '#C0C0C0' => 0x16, '#808080' => 0x17, '#9999FF' => 0x18, '#993366' => 0x19, '#FFFFCC' => 0x1A, '#CCFFFF' => 0x1B, '#660066' => 0x1C, '#FF8080' => 0x1D, '#0066CC' => 0x1E, '#CCCCFF' => 0x1F, // '#000080' => 0x20, // '#FF00FF' => 0x21, // '#FFFF00' => 0x22, // '#00FFFF' => 0x23, // '#800080' => 0x24, // '#800000' => 0x25, // '#008080' => 0x26, // '#0000FF' => 0x27, '#00CCFF' => 0x28, // '#CCFFFF' => 0x29, '#CCFFCC' => 0x2A, '#FFFF99' => 0x2B, '#99CCFF' => 0x2C, '#FF99CC' => 0x2D, '#CC99FF' => 0x2E, '#FFCC99' => 0x2F, '#3366FF' => 0x30, '#33CCCC' => 0x31, '#99CC00' => 0x32, '#FFCC00' => 0x33, '#FF9900' => 0x34, '#FF6600' => 0x35, '#666699' => 0x36, '#969696' => 0x37, '#003366' => 0x38, '#339966' => 0x39, '#003300' => 0x3A, '#333300' => 0x3B, '#993300' => 0x3C, // '#993366' => 0x3D, '#333399' => 0x3E, '#333333' => 0x3F, ]; public static function lookup(Color $color, int $defaultIndex = 0x00): int { $colorRgb = $color->getRGB(); if (is_string($colorRgb) && array_key_exists("#{$colorRgb}", self::$colorMap)) { return self::$colorMap["#{$colorRgb}"]; } // TODO Try and map RGB value to nearest colour within the define pallette // $red = Color::getRed($colorRgb, false); // $green = Color::getGreen($colorRgb, false); // $blue = Color::getBlue($colorRgb, false); // $paletteSpace = 3; // $newColor = ($red * $paletteSpace / 256) * ($paletteSpace * $paletteSpace) + // ($green * $paletteSpace / 256) * $paletteSpace + // ($blue * $paletteSpace / 256); return $defaultIndex; } } PK[hDj Font.phpnu[colorIndex = 0x7FFF; $this->font = $font; } /** * Set the color index. * * @param int $colorIndex */ public function setColorIndex($colorIndex): void { $this->colorIndex = $colorIndex; } /** * Get font record data. * * @return string */ public function writeFont() { $font_outline = 0; $font_shadow = 0; $icv = $this->colorIndex; // Index to color palette if ($this->font->getSuperscript()) { $sss = 1; } elseif ($this->font->getSubscript()) { $sss = 2; } else { $sss = 0; } $bFamily = 0; // Font family $bCharSet = \PhpOffice\PhpSpreadsheet\Shared\Font::getCharsetFromFontName($this->font->getName()); // Character set $record = 0x31; // Record identifier $reserved = 0x00; // Reserved $grbit = 0x00; // Font attributes if ($this->font->getItalic()) { $grbit |= 0x02; } if ($this->font->getStrikethrough()) { $grbit |= 0x08; } if ($font_outline) { $grbit |= 0x10; } if ($font_shadow) { $grbit |= 0x20; } $data = pack( 'vvvvvCCCC', // Fontsize (in twips) $this->font->getSize() * 20, $grbit, // Colour $icv, // Font weight self::mapBold($this->font->getBold()), // Superscript/Subscript $sss, self::mapUnderline($this->font->getUnderline()), $bFamily, $bCharSet, $reserved ); $data .= StringHelper::UTF8toBIFF8UnicodeShort($this->font->getName()); $length = strlen($data); $header = pack('vv', $record, $length); return $header . $data; } /** * Map to BIFF5-BIFF8 codes for bold. * * @param bool $bold * * @return int */ private static function mapBold($bold) { if ($bold) { return 0x2BC; // 700 = Bold font weight } return 0x190; // 400 = Normal font weight } /** * Map of BIFF2-BIFF8 codes for underline styles. * * @var int[] */ private static $mapUnderline = [ \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE => 0x00, \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE => 0x01, \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE => 0x02, \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLEACCOUNTING => 0x21, \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLEACCOUNTING => 0x22, ]; /** * Map underline. * * @param string $underline * * @return int */ private static function mapUnderline($underline) { if (isset(self::$mapUnderline[$underline])) { return self::$mapUnderline[$underline]; } return 0x00; } } PK[roggColor/BIFF5.phpnu[PK[#팯Color/BuiltIn.phpnu[PK[$pgg Color/BIFF8.phpnu[PK[Hl Color.phpnu[PK[MRC4.phpnu[PK[ :$MMFMD5.phpnu[PK[i44 7ErrorCode.phpnu[PK[{AA ;:Escher.phpnu[PK[?!=|Style/CellFont.phpnu[PK[[HH$Style/Border.phpnu[PK[uStyle/FillPattern.phpnu[PK[YÁStyle/CellAlignment.phpnu[PK[s|d TCellDataValidation.phpnu[PK[؜ .Worksheet.phpnu[PK[*a]BIFFwriter.phpnu[PK[(00oyXf.phpnu[PK[ Workbook.phpnu[PK[. KParser.phpnu[PK[a!eStyle/CellFill.phpnu[PK[* CVV&Style/CellBorder.phpnu[PK[o% % *Style/ColorMap.phpnu[PK[hDj 5Font.phpnu[PKB