Xls Reader ListWorksheetInfo and New ListWorksheetDimensions

PR #4687 corrected how Xls Writer generated its Dimensions records. We ignore the Dimensions record on read since it does not affect our processing in the slightest. However, the PR raises the possibility that someone might wish to see the data in the Dimensions record. (The PR did an adequate test for retrieving Dimensions data, but it is not generalizable.) To accommodate such a case, we add a new ListWorksheetDimensions function to Xls Reader, similar to ListWorksheetInfo. As luck would have it, the spreadsheet with which I tested the new function produced incorrect results for ListWorksheetInfo, which was ignoring XLS_TYPE_MULRK records. So I added the necessary code to fix ListWorksheetInfo as well.
This commit is contained in:
oleibman
2025-10-21 07:12:20 -07:00
parent eecfb6712d
commit c3a78e480d
5 changed files with 166 additions and 6 deletions
+10
View File
@@ -294,6 +294,16 @@ class Xls extends XlsBase
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.
*/
+105 -1
View File
@@ -124,6 +124,7 @@ class ListFunctions extends Xls
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);
@@ -131,7 +132,11 @@ class ListFunctions extends Xls
$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);
@@ -160,4 +165,103 @@ class ListFunctions extends Xls
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;
}
}
+5 -5
View File
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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
@@ -165,4 +165,50 @@ class InfoNamesTest extends TestCase
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);
}
}
Binary file not shown.