mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-12 19:16:45 +00:00
Slightly Better Support for Escaped Characters in Xlsx Reader/Writer
See [Discussion 4724](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4724) PhpSpreadsheet converts all control characters (x00-x1f) in strings to and from a form which Excel recognizes (e.g. `x1c` becomes `_x001C_` when writing, and vice versa when reading). There have historically been 3 exceptions which go unconverted - tab (x09), line feed (new line) (x0a), and carriage return (x0d). PR #4536 removed those exceptions, but that caused some problems; these were fixed by PR #4619, but the exceptions were restored. The referenced discussion deals with a spreadsheet with a cell containing `_x000D_`, carriage return. Although the writer no longer converts to that string on output, the reader should be able to handle it on input. In fact, the reader ought to handle any string of the form "underscore x 4-hex-digits underscore", whether or not it represents a control character. And there's an interesting edge case. If a user enters into a cell the string `A_x0030_B`, it needs to be handled as-is. Excel handles this by writing it out as `A_x005F_x0030_B`, i.e. substituting `_x005F_` for the first underscore, so that the reader sees `_x005F_` (converting it to underscore) followed by `x0030_B` (no leading underscore, so no conversion). PhpSpreadsheet could probably handle this by converting all underscores on write, but I am trying to emulate Excel and do it only when needed.
This commit is contained in:
@@ -305,7 +305,29 @@ class StringHelper
|
||||
*/
|
||||
public static function controlCharacterOOXML2PHP(string $textValue): string
|
||||
{
|
||||
return str_replace(self::CONTROL_CHARACTERS_VALUES, self::CONTROL_CHARACTERS_KEYS, $textValue);
|
||||
return Preg::replaceCallback('/_x[0-9A-F]{4}_/', self::toOutChar(...), $textValue);
|
||||
}
|
||||
|
||||
private static function toHexVal(string $char): int
|
||||
{
|
||||
if ($char >= '0' && $char <= '9') {
|
||||
return ord($char) - ord('0');
|
||||
}
|
||||
|
||||
return ord($char) - ord('A') + 10;
|
||||
}
|
||||
|
||||
/** @param array<?string> $match */
|
||||
private static function toOutChar(array $match): string
|
||||
{
|
||||
/** @var string */
|
||||
$chars = $match[0];
|
||||
$t = ((self::toHexVal($chars[2]) << 12)
|
||||
| (self::toHexVal($chars[3]) << 8)
|
||||
| (self::toHexVal($chars[4]) << 4)
|
||||
| (self::toHexVal($chars[5])));
|
||||
|
||||
return mb_chr($t, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,6 +345,8 @@ class StringHelper
|
||||
*/
|
||||
public static function controlCharacterPHP2OOXML(string $textValue): string
|
||||
{
|
||||
$textValue = Preg::replace('/_(x[0-9A-F]{4}_)/', '_x005F_$1', $textValue);
|
||||
|
||||
return str_replace(self::CONTROL_CHARACTERS_KEYS, self::CONTROL_CHARACTERS_VALUES, $textValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
class UnderscoreTest extends AbstractFunctional
|
||||
{
|
||||
private ?Spreadsheet $spreadsheet = null;
|
||||
|
||||
private ?Spreadsheet $reloadedSpreadsheet = null;
|
||||
|
||||
private const TEST_FILE = 'tests/data/Reader/XLSX/issue.4724.xlsx';
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->spreadsheet !== null) {
|
||||
$this->spreadsheet->disconnectWorksheets();
|
||||
$this->spreadsheet = null;
|
||||
}
|
||||
if ($this->reloadedSpreadsheet !== null) {
|
||||
$this->reloadedSpreadsheet->disconnectWorksheets();
|
||||
$this->reloadedSpreadsheet = null;
|
||||
}
|
||||
}
|
||||
|
||||
#[DataProvider('underscoreProvider')]
|
||||
public function testUnderscore(string $value): void
|
||||
{
|
||||
$this->spreadsheet = new Spreadsheet();
|
||||
$sheet = $this->spreadsheet->getActiveSheet();
|
||||
$sheet->setCellValue('A1', $value);
|
||||
$this->reloadedSpreadsheet = $this->writeAndReload($this->spreadsheet, 'Xlsx');
|
||||
$rsheet = $this->reloadedSpreadsheet->getActiveSheet();
|
||||
self::assertSame($value, $rsheet->getCell('A1')->getValue());
|
||||
}
|
||||
|
||||
#[DataProvider('underscoreProvider')]
|
||||
public function testUnderscoreInline(string $value): void
|
||||
{
|
||||
$this->spreadsheet = new Spreadsheet();
|
||||
$sheet = $this->spreadsheet->getActiveSheet();
|
||||
$sheet->setCellValueExplicit('A1', $value, DataType::TYPE_INLINE);
|
||||
$this->reloadedSpreadsheet = $this->writeAndReload($this->spreadsheet, 'Xlsx');
|
||||
$rsheet = $this->reloadedSpreadsheet->getActiveSheet();
|
||||
self::assertSame($value, $rsheet->getCell('A1')->getValueString());
|
||||
}
|
||||
|
||||
public static function underscoreProvider(): array
|
||||
{
|
||||
return [
|
||||
['A_x0030_'],
|
||||
['A_x0030_B'],
|
||||
['A_B'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testPreliminaries(): void
|
||||
{
|
||||
$file = 'zip://';
|
||||
$file .= self::TEST_FILE;
|
||||
$file .= '#xl/sharedStrings.xml';
|
||||
$data = file_get_contents($file);
|
||||
if ($data === false) {
|
||||
self::fail('Unable to read file');
|
||||
} else {
|
||||
self::assertStringContainsString('count="3"', $data);
|
||||
self::assertStringContainsString(
|
||||
"<t>line_x000D_\nwith_x000D_\nbreaks</t>",
|
||||
$data
|
||||
);
|
||||
self::assertStringContainsString('<t>A_x005F_x0030_B</t>', $data);
|
||||
self::assertStringContainsString(
|
||||
'<t>ABC_x0031__x0032__x0033_DEF</t>',
|
||||
$data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testX000dPreserved(): void
|
||||
{
|
||||
$reader = new XlsxReader();
|
||||
$binder = new DefaultValueBinder();
|
||||
$binder->setPreserveCr(true);
|
||||
$reader->setValueBinder($binder);
|
||||
$infile = self::TEST_FILE;
|
||||
$this->spreadsheet = $reader->load($infile);
|
||||
$sheet = $this->spreadsheet->getActiveSheet();
|
||||
$expected = "line\r\nwith\r\nbreaks";
|
||||
self::assertSame($expected, $sheet->getCell('A1')->getValue());
|
||||
$expected = 'A_x0030_B';
|
||||
self::assertSame($expected, $sheet->getCell('A2')->getValue());
|
||||
$expected = 'ABC123DEF';
|
||||
self::assertSame($expected, $sheet->getCell('A3')->getValue());
|
||||
}
|
||||
|
||||
public function testX000dNotPreserved(): void
|
||||
{
|
||||
$reader = new XlsxReader();
|
||||
$infile = self::TEST_FILE;
|
||||
$this->spreadsheet = $reader->load($infile);
|
||||
$sheet = $this->spreadsheet->getActiveSheet();
|
||||
$expected = "line\nwith\nbreaks";
|
||||
self::assertSame($expected, $sheet->getCell('A1')->getValue());
|
||||
$expected = 'A_x0030_B';
|
||||
self::assertSame($expected, $sheet->getCell('A2')->getValue());
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user