Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a

- Option to display numbers with less precision. [Issue #4626](https://github.com/PHPOffice/PhpSpreadsheet/issues/4626) [PR #4640](https://github.com/PHPOffice/PhpSpreadsheet/pull/4640)
- Offer Tcpdf Interface which throws exception rather than die. [PR #4666](https://github.com/PHPOffice/PhpSpreadsheet/pull/4666)
- Xls Reader ListWorksheetDimensions method. [PR #4689](https://github.com/PHPOffice/PhpSpreadsheet/pull/4689)

### Removed

Expand Down Expand Up @@ -45,6 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
- Better support for Style Alignment Read Order. [Issue #850](https://github.com/PHPOffice/PhpSpreadsheet/issues/850) [PR #4655](https://github.com/PHPOffice/PhpSpreadsheet/pull/4655)
- More sophisticated workbook password algorithms (Xlsx only). [Issue #4673](https://github.com/PHPOffice/PhpSpreadsheet/issues/4673) [PR #4675](https://github.com/PHPOffice/PhpSpreadsheet/pull/4675)
- Xls Writer fix DIMENSIONS record. [Issue #4682](https://github.com/PHPOffice/PhpSpreadsheet/issues/4682) [PR #4687](https://github.com/PHPOffice/PhpSpreadsheet/pull/4687)
- Xls Reader listWorksheetInfo process MULRK records. [PR #4689](https://github.com/PHPOffice/PhpSpreadsheet/pull/4689)
- Make Reader Xls Escher generic. [PR #4690](https://github.com/PHPOffice/PhpSpreadsheet/pull/4690)

## 2025-09-03 - 5.1.0

Expand Down
10 changes: 10 additions & 0 deletions src/PhpSpreadsheet/Reader/Xls.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,16 @@ public function listWorksheetInfo(string $filename): array
return (new Xls\ListFunctions())->listWorksheetInfo2($filename, $this);
}

/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
* @return array<int, array{worksheetName: string, dimensionsMinR: int, dimensionsMinC: int, dimensionsMaxR: int, dimensionsMaxC: int, lastColumnLetter: string}>
*/
public function listWorksheetDimensions(string $filename): array
{
return (new Xls\ListFunctions())->listWorksheetDimensions2($filename, $this);
}

/**
* Loads PhpSpreadsheet from file.
*/
Expand Down
106 changes: 105 additions & 1 deletion src/PhpSpreadsheet/Reader/Xls/ListFunctions.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,19 @@ protected function listWorksheetInfo2(string $filename, Xls $xls): array
case self::XLS_TYPE_FORMULA:
case self::XLS_TYPE_BOOLERR:
case self::XLS_TYPE_LABEL:
case self::XLS_TYPE_MULRK:
$length = self::getUInt2d($xls->data, $xls->pos + 2);
$recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length);

// move stream pointer to next record
$xls->pos += 4 + $length;

$rowIndex = self::getUInt2d($recordData, 0) + 1;
$columnIndex = self::getUInt2d($recordData, 2);
if ($code === self::XLS_TYPE_MULRK) {
$columnIndex = self::getUInt2d($recordData, $length - 2);
} else {
$columnIndex = self::getUInt2d($recordData, 2);
}

$tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
$tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
Expand Down Expand Up @@ -160,4 +165,103 @@ protected function listWorksheetInfo2(string $filename, Xls $xls): array

return $worksheetInfo;
}

/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
* @return array<int, array{worksheetName: string, dimensionsMinR: int, dimensionsMinC: int, dimensionsMaxR: int, dimensionsMaxC: int, lastColumnLetter: string}>
*/
protected function listWorksheetDimensions2(string $filename, Xls $xls): array
{
File::assertFile($filename);

$worksheetInfo = [];

// Read the OLE file
$xls->loadOLE($filename);

// total byte size of Excel data (workbook global substream + sheet substreams)
$xls->dataSize = strlen($xls->data);

// initialize
$xls->pos = 0;
$xls->sheets = [];

// Parse Workbook Global Substream
while ($xls->pos < $xls->dataSize) {
$code = self::getUInt2d($xls->data, $xls->pos);

match ($code) {
self::XLS_TYPE_BOF => $xls->readBof(),
self::XLS_TYPE_SHEET => $xls->readSheet(),
self::XLS_TYPE_EOF => $xls->readDefault(),
self::XLS_TYPE_CODEPAGE => $xls->readCodepage(),
default => $xls->readDefault(),
};

if ($code === self::XLS_TYPE_EOF) {
break;
}
}

// Parse the individual sheets
foreach ($xls->sheets as $sheet) {
if ($sheet['sheetType'] !== 0x00) {
// 0x00: Worksheet
// 0x02: Chart
// 0x06: Visual Basic module
continue;
}

$tmpInfo = [];
$tmpInfo['worksheetName'] = StringHelper::convertToString($sheet['name']);
$tmpInfo['dimensionsMinR'] = -1;
$tmpInfo['dimensionsMaxR'] = -1;
$tmpInfo['dimensionsMinC'] = -1;
$tmpInfo['dimensionsMaxC'] = -1;
$tmpInfo['lastColumnLetter'] = '';

$xls->pos = $sheet['offset'];

while ($xls->pos <= $xls->dataSize - 4) {
$code = self::getUInt2d($xls->data, $xls->pos);

switch ($code) {
case self::XLS_TYPE_BOF:
$xls->readBof();

break;
case self::XLS_TYPE_EOF:
$xls->readDefault();

break 2;
case self::XLS_TYPE_DIMENSION:
$length = self::getUInt2d($xls->data, $xls->pos + 2);
if ($length === 14) {
$dimensionsData = substr($xls->data, $xls->pos + 4, $length);
$data = unpack('VrwMic/VrwMac/vcolMic/vcolMac/vreserved', $dimensionsData);
if (is_array($data)) {
/** @var int[] $data */
$tmpInfo['dimensionsMinR'] = $data['rwMic'];
$tmpInfo['dimensionsMaxR'] = $data['rwMac'];
$tmpInfo['dimensionsMinC'] = $data['colMic'];
$tmpInfo['dimensionsMaxC'] = $data['colMac'];
$tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['dimensionsMaxC']);
}
}
$xls->readDefault();

break;
default:
$xls->readDefault();

break;
}
}

$worksheetInfo[] = $tmpInfo;
}

return $worksheetInfo;
}
}
10 changes: 5 additions & 5 deletions src/PhpSpreadsheet/Reader/XlsBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ class XlsBase extends BaseReader
final const XLS_TYPE_FORMULA = 0x0006;
final const XLS_TYPE_EOF = 0x000A;
final const XLS_TYPE_PROTECT = 0x0012;
final const XLS_TYPE_OBJECTPROTECT = 0x0063;
final const XLS_TYPE_SCENPROTECT = 0x00DD;
final const XLS_TYPE_PASSWORD = 0x0013;
final const XLS_TYPE_HEADER = 0x0014;
final const XLS_TYPE_FOOTER = 0x0015;
Expand All @@ -50,6 +48,7 @@ class XlsBase extends BaseReader
final const XLS_TYPE_CODEPAGE = 0x0042;
final const XLS_TYPE_DEFCOLWIDTH = 0x0055;
final const XLS_TYPE_OBJ = 0x005D;
final const XLS_TYPE_OBJECTPROTECT = 0x0063;
final const XLS_TYPE_COLINFO = 0x007D;
final const XLS_TYPE_IMDATA = 0x007F;
final const XLS_TYPE_SHEETPR = 0x0081;
Expand All @@ -62,6 +61,7 @@ class XlsBase extends BaseReader
final const XLS_TYPE_MULRK = 0x00BD;
final const XLS_TYPE_MULBLANK = 0x00BE;
final const XLS_TYPE_DBCELL = 0x00D7;
final const XLS_TYPE_SCENPROTECT = 0x00DD;
final const XLS_TYPE_XF = 0x00E0;
final const XLS_TYPE_MERGEDCELLS = 0x00E5;
final const XLS_TYPE_MSODRAWINGGROUP = 0x00EB;
Expand All @@ -70,6 +70,8 @@ class XlsBase extends BaseReader
final const XLS_TYPE_LABELSST = 0x00FD;
final const XLS_TYPE_EXTSST = 0x00FF;
final const XLS_TYPE_EXTERNALBOOK = 0x01AE;
final const XLS_TYPE_CFHEADER = 0x01B0;
final const XLS_TYPE_CFRULE = 0x01B1;
final const XLS_TYPE_DATAVALIDATIONS = 0x01B2;
final const XLS_TYPE_TXO = 0x01B6;
final const XLS_TYPE_HYPERLINK = 0x01B8;
Expand All @@ -90,13 +92,11 @@ class XlsBase extends BaseReader
final const XLS_TYPE_FORMAT = 0x041E;
final const XLS_TYPE_SHAREDFMLA = 0x04BC;
final const XLS_TYPE_BOF = 0x0809;
final const XLS_TYPE_SHEETLAYOUT = 0x0862;
final const XLS_TYPE_SHEETPROTECTION = 0x0867;
final const XLS_TYPE_RANGEPROTECTION = 0x0868;
final const XLS_TYPE_SHEETLAYOUT = 0x0862;
final const XLS_TYPE_XFEXT = 0x087D;
final const XLS_TYPE_PAGELAYOUTVIEW = 0x088B;
final const XLS_TYPE_CFHEADER = 0x01B0;
final const XLS_TYPE_CFRULE = 0x01B1;
final const XLS_TYPE_UNKNOWN = 0xFFFF;

// Encryption type
Expand Down
76 changes: 76 additions & 0 deletions tests/PhpSpreadsheetTests/Reader/Xls/InfoNamesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,80 @@ public function testLoadMacCentralEuropeBiff8(): void
self::assertSame('Użytkownik Microsoft Office', $properties->getLastModifiedBy());
$spreadsheet->disconnectWorksheets();
}

public function testDimensions(): void
{
$filename = 'tests/data/Reader/XLS/pr.4687.excel.xls';
$reader = new Xls();
$info = $reader->listWorksheetInfo($filename);
$expected = [
[
'worksheetName' => 'Sheet1',
'lastColumnLetter' => 'D',
'lastColumnIndex' => 3,
'totalRows' => 2,
'totalColumns' => 4,
'sheetState' => 'visible',
],
[
'worksheetName' => 'Sheet2',
'lastColumnLetter' => 'B',
'lastColumnIndex' => 1,
'totalRows' => 4,
'totalColumns' => 2,
'sheetState' => 'visible',
],
];
self::assertSame($expected, $info);
$info = $reader->listWorksheetDimensions($filename);
$expected = [
[
'worksheetName' => 'Sheet1',
'dimensionsMinR' => 0,
'dimensionsMaxR' => 2,
'dimensionsMinC' => 0,
'dimensionsMaxC' => 4,
'lastColumnLetter' => 'D',
],
[
'worksheetName' => 'Sheet2',
'dimensionsMinR' => 0,
'dimensionsMaxR' => 4,
'dimensionsMinC' => 0,
'dimensionsMaxC' => 2,
'lastColumnLetter' => 'B',
],
];
self::assertSame($expected, $info);
}

public function testChartSheetIgnored(): void
{
$filename = 'tests/data/Reader/XLS/chartsheet.xls';
$reader = new Xls();
$info = $reader->listWorksheetInfo($filename);
$expected = [
[
'worksheetName' => 'Data',
'lastColumnLetter' => 'M',
'lastColumnIndex' => 12,
'totalRows' => 7,
'totalColumns' => 13,
'sheetState' => 'visible',
],
];
self::assertSame($expected, $info);
$info = $reader->listWorksheetDimensions($filename);
$expected = [
[
'worksheetName' => 'Data',
'dimensionsMinR' => 0,
'dimensionsMaxR' => 7,
'dimensionsMinC' => 0,
'dimensionsMaxC' => 13,
'lastColumnLetter' => 'M',
],
];
self::assertSame($expected, $info);
}
}
36 changes: 36 additions & 0 deletions tests/PhpSpreadsheetTests/Shared/OLETest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,40 @@ public function testChainedBadPath(): void
}
}
}

public function testOleFunctions(): void
{
$ole = new OLE();
$infile = 'tests/data/Reader/XLS/pr.4687.excel.xls';
$ole->read($infile);
self::assertSame(4, $ole->ppsTotal());
self::assertFalse($ole->isFile(0), 'root entry');
self::assertTrue($ole->isFile(1), 'workbook');
self::assertTrue($ole->isFile(2), 'summary information');
self::assertTrue($ole->isFile(3), 'document summary information');
self::assertFalse($ole->isFile(4), 'no such index');
self::assertTrue($ole->isRoot(0), 'root entry');
self::assertFalse($ole->isRoot(1), 'workbook');
self::assertFalse($ole->isRoot(2), 'summary information');
self::assertFalse($ole->isRoot(3), 'document summary information');
self::assertFalse($ole->isRoot(4), 'no such index');
self::assertSame(0, $ole->getDataLength(0), 'root entry');
self::assertSame(15712, $ole->getDataLength(1), 'workbook');
self::assertSame(4096, $ole->getDataLength(2), 'summary information');
self::assertSame(4096, $ole->getDataLength(3), 'document summary information');
self::assertSame(0, $ole->getDataLength(4), 'no such index');
self::assertSame('', $ole->getData(2, -1, 4), 'negative position');
self::assertSame('', $ole->getData(2, 5000, 4), 'position > length');
self::assertSame('feff0000', bin2hex($ole->getData(2, 0, 4)));
self::assertSame('', $ole->getData(4, 0, 4), 'no such index');
}

public function testBadEndian(): void
{
$this->expectException(ReaderException::class);
$this->expectExceptionMessage('Only Little-Endian encoding is supported');
$ole = new OLE();
$infile = 'tests/data/Reader/XLS/pr.4687.excel.badendian.xls';
$ole->read($infile);
}
}
26 changes: 26 additions & 0 deletions tests/PhpSpreadsheetTests/Shared/XlsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Shared;

use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;

class XlsTest extends TestCase
{
public function testSizes(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray([[1, 2], [3, 4]]);
$sheet->getColumnDimension('B')->setVisible(false);
$sheet->getRowDimension(2)->setVisible(false);
self::assertSame(64, SharedXls::sizeCol($sheet, 'A'));
self::assertSame(0, SharedXls::sizeCol($sheet, 'B'));
self::assertSame(20, SharedXls::sizeRow($sheet, 1));
self::assertSame(0, SharedXls::sizeRow($sheet, 2));
$spreadsheet->disconnectWorksheets();
}
}
Binary file added tests/data/Reader/XLS/chartsheet.xls
Binary file not shown.
Binary file not shown.
Binary file added tests/data/Reader/XLS/pr.4687.excel.xls
Binary file not shown.