Repository: PHPOffice/PhpSpreadsheet (php)

Issue:
Incorrect handling of EOL characters in cell values
When spreadsheet is saved in OOXML file format CR character must be encoded according to ISO/IEC 29500-1 (Office Open XML File Formats Part 1) standard (as `_x000D_`) or XML standard (as `&#xD;`).

When reading cell value from OOXML file PHPSpreadsheet file reader must decode `_x000D_` into CR character (`&#xD;` will be decoded automatically by PHP's XML reader used by PHPSpreadsheet file reader).

Microsoft Excel correctly handles CR character in cell values encoded either way (the character is not lost when resaving the XLSX file in Microsoft Excel).

### What is the expected behavior?
CR character survive a round-trip through Xlsx and Open Office XML formats.
```
true
false
```
### What is the current behavior?
CR character is lost after a round-trip through Xlsx and Open Office XML formats.
```
false
true
```
### What are the steps to reproduce?
```php
<?php
require __DIR__ . '/vendor/autoload.php';
$s1 = "AA\r\nAA";
$doc = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $doc->getSheet(0);
$sheet->getCellByColumnAndRow(1, 1)->setValueExplicit($s1, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($doc, 'Xlsx');
$writer->save('1.xlsx');

$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$doc = $reader->load('1.xlsx');
$sheet = $doc->getSheet(0);
$s2 = $sheet->getCellByColumnAndRow(1, 1)->getValue();
var_dump($s2 === $s1);
var_dump($s2 === "AA\nAA");
```

### Which versions of PhpSpreadsheet and PHP are affected?
1.0.0.

Pull request description:
Preserve 0x0a In Strings If Desired
Fix #347, which had gone stale but is now reopened. PhpSpreadsheet is automatically replacing CR-LF and CR with LF. The reason for this is unknown, although I suggest a possibility in the issue. I am not willing to make this a breaking change. This PR adds a new property `preserveCr` with setter and getter to DefaultValueBinder (and, by extension, to StringValueBinder and AdvancedValueBinder). The property defaults to false, but users who wish to preserve CR-LF and CR can set it to true for a Spreadsheet or a Reader.

This is:

- [x] a bugfix
- [ ] a new feature
- [ ] refactoring
- [ ] additional unit tests

Checklist:

- [ ] Changes are covered by unit tests
  - [ ] Changes are covered by existing unit tests
  - [x] New unit tests have been added
- [x] Code style is respected
- [x] Commit message explains **why** the change is made (see https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
- [ ] CHANGELOG.md contains a short summary of the change and a link to the pull request if applicable
- [ ] Documentation is updated as necessary

### Why this change is needed?

Provide an explanation of why this change is needed, with links to any Issues (if appropriate).
If this is a bugfix or a new feature, and there are no existing Issues, then please also create an issue that will make it easier to track progress with this PR.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 467d3ee83..d248136cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
 - Recognize application/x-empty mimetype. [Issue #4521](https://github.com/PHPOffice/PhpSpreadsheet/issues/4521) [PR #4524](https://github.com/PHPOffice/PhpSpreadsheet/pull/4524)
 - Micro-optimization in getSheetByName. [PR #4499](https://github.com/PHPOffice/PhpSpreadsheet/pull/4499)
 - Bug in resizeMatricesExtend. [Issue #4451](https://github.com/PHPOffice/PhpSpreadsheet/issues/4451) [PR #4474](https://github.com/PHPOffice/PhpSpreadsheet/pull/4474)
+- Preserve 0x0a in Strings if Desired. [Issue #347](https://github.com/PHPOffice/PhpSpreadsheet/issues/347) [PR #4536](https://github.com/PHPOffice/PhpSpreadsheet/pull/4536)
 
 ## 2025-06-22 - 4.4.0
 
diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php
index b53d1d1e0..8cf71f9d9 100644
--- a/src/PhpSpreadsheet/Cell/Cell.php
+++ b/src/PhpSpreadsheet/Cell/Cell.php
@@ -278,7 +278,14 @@ class Cell implements Stringable
             case DataType::TYPE_INLINE:
                 // Rich text
                 $value2 = StringHelper::convertToString($value, true);
-                $this->value = DataType::checkString(($value instanceof RichText) ? $value : $value2);
+                // Cells?->Worksheet?->Spreadsheet
+                $binder = $this->parent?->getParent()?->getParent()?->getValueBinder();
+                $preserveCr = false;
+                if ($binder !== null && method_exists($binder, 'getPreserveCr')) {
+                    /** @var bool */
+                    $preserveCr = $binder->getPreserveCr();
+                }
+                $this->value = DataType::checkString(($value instanceof RichText) ? $value : $value2, $preserveCr);
 
                 break;
             case DataType::TYPE_NUMERIC:
diff --git a/src/PhpSpreadsheet/Cell/DataType.php b/src/PhpSpreadsheet/Cell/DataType.php
index 39969d37a..774a57b23 100644
--- a/src/PhpSpreadsheet/Cell/DataType.php
+++ b/src/PhpSpreadsheet/Cell/DataType.php
@@ -53,7 +53,7 @@ class DataType
      *
      * @return RichText|string Sanitized value
      */
-    public static function checkString(null|RichText|string $textValue): RichText|string
+    public static function checkString(null|RichText|string $textValue, bool $preserveCr = false): RichText|string
     {
         if ($textValue instanceof RichText) {
             // TODO: Sanitize Rich-Text string (max. character count is 32,767)
@@ -64,7 +64,9 @@ class DataType
         $textValue = StringHelper::substring((string) $textValue, 0, self::MAX_STRING_LENGTH);
 
         // we require that newline is represented as "\n" in core, not as "\r\n" or "\r"
-        $textValue = str_replace(["\r\n", "\r"], "\n", $textValue);
+        if (!$preserveCr) {
+            $textValue = str_replace(["\r\n", "\r"], "\n", $textValue);
+        }
 
         return $textValue;
     }
diff --git a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php
index 10c5c93c5..0d7f286f7 100644
--- a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php
+++ b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php
@@ -108,4 +108,18 @@ class DefaultValueBinder implements IValueBinder
 
         return DataType::TYPE_STRING;
     }
+
+    protected bool $preserveCr = false;
+
+    public function getPreserveCr(): bool
+    {
+        return $this->preserveCr;
+    }
+
+    public function setPreserveCr(bool $preserveCr): self
+    {
+        $this->preserveCr = $preserveCr;
+
+        return $this;
+    }
 }
diff --git a/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php b/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php
index 60fc5a125..fb28f398f 100644
--- a/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php
+++ b/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php
@@ -27,7 +27,7 @@ class LoadSpreadsheet extends Xls
 
         // Initialisations
         $xls->spreadsheet = $this->newSpreadsheet();
-        $xls->spreadsheet->setValueBinder($this->valueBinder);
+        $xls->spreadsheet->setValueBinder($xls->valueBinder);
         $xls->spreadsheet->removeSheetByIndex(0); // remove 1st sheet
         if (!$xls->readDataOnly) {
             $xls->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style
diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php
index 045c7b472..323aff408 100644
--- a/src/PhpSpreadsheet/Shared/StringHelper.php
+++ b/src/PhpSpreadsheet/Shared/StringHelper.php
@@ -9,19 +9,236 @@ use Stringable;
 
 class StringHelper
 {
-    /**
-     * Control characters array.
-     *
-     * @var string[]
-     */
-    private static array $controlCharacters = [];
+    private const CONTROL_CHARACTERS_KEYS = [
+        "\x00",
+        "\x01",
+        "\x02",
+        "\x03",
+        "\x04",
+        "\x05",
+        "\x06",
+        "\x07",
+        "\x08",
+        "\x09",
+        "\x0a",
+        "\x0b",
+        "\x0c",
+        "\x0d",
+        "\x0e",
+        "\x0f",
+        "\x10",
+        "\x11",
+        "\x12",
+        "\x13",
+        "\x14",
+        "\x15",
+        "\x16",
+        "\x17",
+        "\x18",
+        "\x19",
+        "\x1a",
+        "\x1b",
+        "\x1c",
+        "\x1d",
+        "\x1e",
+        "\x1f",
+    ];
+    private const CONTROL_CHARACTERS_VALUES = [
+        '_x0000_',
+        '_x0001_',
+        '_x0002_',
+        '_x0003_',
+        '_x0004_',
+        '_x0005_',
+        '_x0006_',
+        '_x0007_',
+        '_x0008_',
+        '_x0009_',
+        '_x000A_',
+        '_x000B_',
+        '_x000C_',
+        '_x000D_',
+        '_x000E_',
+        '_x000F_',
+        '_x0010_',
+        '_x0011_',
+        '_x0012_',
+        '_x0013_',
+        '_x0014_',
+        '_x0015_',
+        '_x0016_',
+        '_x0017_',
+        '_x0018_',
+        '_x0019_',
+        '_x001A_',
+        '_x001B_',
+        '_x001C_',
+        '_x001D_',
+        '_x001E_',
+        '_x001F_',
+    ];
 
     /**
      * SYLK Characters array.
-     *
-     * @var string[]
      */
-    private static array $SYLKCharacters = [];
+    private const SYLK_CHARACTERS = [
+        "\x1B 0" => "\x00",
+        "\x1B 1" => "\x01",
+        "\x1B 2" => "\x02",
+        "\x1B 3" => "\x03",
+        "\x1B 4" => "\x04",
+        "\x1B 5" => "\x05",
+        "\x1B 6" => "\x06",
+        "\x1B 7" => "\x07",
+        "\x1B 8" => "\x08",
+        "\x1B 9" => "\x09",
+        "\x1B :" => "\x0a",
+        "\x1B ;" => "\x0b",
+        "\x1B <" => "\x0c",
+        "\x1B =" => "\x0d",
+        "\x1B >" => "\x0e",
+        "\x1B ?" => "\x0f",
+        "\x1B!0" => "\x10",
+        "\x1B!1" => "\x11",
+        "\x1B!2" => "\x12",
+        "\x1B!3" => "\x13",
+        "\x1B!4" => "\x14",
+        "\x1B!5" => "\x15",
+        "\x1B!6" => "\x16",
+        "\x1B!7" => "\x17",
+        "\x1B!8" => "\x18",
+        "\x1B!9" => "\x19",
+        "\x1B!:" => "\x1a",
+        "\x1B!;" => "\x1b",
+        "\x1B!<" => "\x1c",
+        "\x1B!=" => "\x1d",
+        "\x1B!>" => "\x1e",
+        "\x1B!?" => "\x1f",
+        "\x1B'?" => "\x7f",
+        "\x1B(0" => '€', // 128 in CP1252
+        "\x1B(2" => '‚', // 130 in CP1252
+        "\x1B(3" => 'ƒ', // 131 in CP1252
+        "\x1B(4" => '„', // 132 in CP1252
+        "\x1B(5" => '…', // 133 in CP1252
+        "\x1B(6" => '†', // 134 in CP1252
+        "\x1B(7" => '‡', // 135 in CP1252
+        "\x1B(8" => 'ˆ', // 136 in CP1252
+        "\x1B(9" => '‰', // 137 in CP1252
+        "\x1B(:" => 'Š', // 138 in CP1252
+        "\x1B(;" => '‹', // 139 in CP1252
+        "\x1BNj" => 'Œ', // 140 in CP1252
+        "\x1B(>" => 'Ž', // 142 in CP1252
+        "\x1B)1" => '‘', // 145 in CP1252
+        "\x1B)2" => '’', // 146 in CP1252
+        "\x1B)3" => '“', // 147 in CP1252
+        "\x1B)4" => '”', // 148 in CP1252
+        "\x1B)5" => '•', // 149 in CP1252
+        "\x1B)6" => '–', // 150 in CP1252
+        "\x1B)7" => '—', // 151 in CP1252
+        "\x1B)8" => '˜', // 152 in CP1252
+        "\x1B)9" => '™', // 153 in CP1252
+        "\x1B):" => 'š', // 154 in CP1252
+        "\x1B);" => '›', // 155 in CP1252
+        "\x1BNz" => 'œ', // 156 in CP1252
+        "\x1B)>" => 'ž', // 158 in CP1252
+        "\x1B)?" => 'Ÿ', // 159 in CP1252
+        "\x1B*0" => ' ', // 160 in CP1252
+        "\x1BN!" => '¡', // 161 in CP1252
+        "\x1BN\"" => '¢', // 162 in CP1252
+        "\x1BN#" => '£', // 163 in CP1252
+        "\x1BN(" => '¤', // 164 in CP1252
+        "\x1BN%" => '¥', // 165 in CP1252
+        "\x1B*6" => '¦', // 166 in CP1252
+        "\x1BN'" => '§', // 167 in CP1252
+        "\x1BNH " => '¨', // 168 in CP1252
+        "\x1BNS" => '©', // 169 in CP1252
+        "\x1BNc" => 'ª', // 170 in CP1252
+        "\x1BN+" => '«', // 171 in CP1252
+        "\x1B*<" => '¬', // 172 in CP1252
+        "\x1B*=" => '­', // 173 in CP1252
+        "\x1BNR" => '®', // 174 in CP1252
+        "\x1B*?" => '¯', // 175 in CP1252
+        "\x1BN0" => '°', // 176 in CP1252
+        "\x1BN1" => '±', // 177 in CP1252
+        "\x1BN2" => '²', // 178 in CP1252
+        "\x1BN3" => '³', // 179 in CP1252
+        "\x1BNB " => '´', // 180 in CP1252
+        "\x1BN5" => 'µ', // 181 in CP1252
+        "\x1BN6" => '¶', // 182 in CP1252
+        "\x1BN7" => '·', // 183 in CP1252
+        "\x1B+8" => '¸', // 184 in CP1252
+        "\x1BNQ" => '¹', // 185 in CP1252
+        "\x1BNk" => 'º', // 186 in CP1252
+        "\x1BN;" => '»', // 187 in CP1252
+        "\x1BN<" => '¼', // 188 in CP1252
+        "\x1BN=" => '½', // 189 in CP1252
+        "\x1BN>" => '¾', // 190 in CP1252
+        "\x1BN?" => '¿', // 191 in CP1252
+        "\x1BNAA" => 'À', // 192 in CP1252
+        "\x1BNBA" => 'Á', // 193 in CP1252
+        "\x1BNCA" => 'Â', // 194 in CP1252
+        "\x1BNDA" => 'Ã', // 195 in CP1252
+        "\x1BNHA" => 'Ä', // 196 in CP1252
+        "\x1BNJA" => 'Å', // 197 in CP1252
+        "\x1BNa" => 'Æ', // 198 in CP1252
+        "\x1BNKC" => 'Ç', // 199 in CP1252
+        "\x1BNAE" => 'È', // 200 in CP1252
+        "\x1BNBE" => 'É', // 201 in CP1252
+        "\x1BNCE" => 'Ê', // 202 in CP1252
+        "\x1BNHE" => 'Ë', // 203 in CP1252
+        "\x1BNAI" => 'Ì', // 204 in CP1252
+        "\x1BNBI" => 'Í', // 205 in CP1252
+        "\x1BNCI" => 'Î', // 206 in CP1252
+        "\x1BNHI" => 'Ï', // 207 in CP1252
+        "\x1BNb" => 'Ð', // 208 in CP1252
+        "\x1BNDN" => 'Ñ', // 209 in CP1252
+        "\x1BNAO" => 'Ò', // 210 in CP1252
+        "\x1BNBO" => 'Ó', // 211 in CP1252
+        "\x1BNCO" => 'Ô', // 212 in CP1252
+        "\x1BNDO" => 'Õ', // 213 in CP1252
+        "\x1BNHO" => 'Ö', // 214 in CP1252
+        "\x1B-7" => '×', // 215 in CP1252
+        "\x1BNi" => 'Ø', // 216 in CP1252
+        "\x1BNAU" => 'Ù', // 217 in CP1252
+        "\x1BNBU" => 'Ú', // 218 in CP1252
+        "\x1BNCU" => 'Û', // 219 in CP1252
+        "\x1BNHU" => 'Ü', // 220 in CP1252
+        "\x1B-=" => 'Ý', // 221 in CP1252
+        "\x1BNl" => 'Þ', // 222 in CP1252
+        "\x1BN{" => 'ß', // 223 in CP1252
+        "\x1BNAa" => 'à', // 224 in CP1252
+        "\x1BNBa" => 'á', // 225 in CP1252
+        "\x1BNCa" => 'â', // 226 in CP1252
+        "\x1BNDa" => 'ã', // 227 in CP1252
+        "\x1BNHa" => 'ä', // 228 in CP1252
+        "\x1BNJa" => 'å', // 229 in CP1252
+        "\x1BNq" => 'æ', // 230 in CP1252
+        "\x1BNKc" => 'ç', // 231 in CP1252
+        "\x1BNAe" => 'è', // 232 in CP1252
+        "\x1BNBe" => 'é', // 233 in CP1252
+        "\x1BNCe" => 'ê', // 234 in CP1252
+        "\x1BNHe" => 'ë', // 235 in CP1252
+        "\x1BNAi" => 'ì', // 236 in CP1252
+        "\x1BNBi" => 'í', // 237 in CP1252
+        "\x1BNCi" => 'î', // 238 in CP1252
+        "\x1BNHi" => 'ï', // 239 in CP1252
+        "\x1BNs" => 'ð', // 240 in CP1252
+        "\x1BNDn" => 'ñ', // 241 in CP1252
+        "\x1BNAo" => 'ò', // 242 in CP1252
+        "\x1BNBo" => 'ó', // 243 in CP1252
+        "\x1BNCo" => 'ô', // 244 in CP1252
+        "\x1BNDo" => 'õ', // 245 in CP1252
+        "\x1BNHo" => 'ö', // 246 in CP1252
+        "\x1B/7" => '÷', // 247 in CP1252
+        "\x1BNy" => 'ø', // 248 in CP1252
+        "\x1BNAu" => 'ù', // 249 in CP1252
+        "\x1BNBu" => 'ú', // 250 in CP1252
+        "\x1BNCu" => 'û', // 251 in CP1252
+        "\x1BNHu" => 'ü', // 252 in CP1252
+        "\x1B/=" => 'ý', // 253 in CP1252
+        "\x1BN|" => 'þ', // 254 in CP1252
+        "\x1BNHy" => 'ÿ', // 255 in CP1252
+    ];
 
     /**
      * Decimal separator.
@@ -48,185 +265,6 @@ class StringHelper
      */
     private static string $iconvOptions = '//IGNORE//TRANSLIT';
 
-    /**
-     * Build control characters array.
-     */
-    private static function buildControlCharacters(): void
-    {
-        for ($i = 0; $i <= 31; ++$i) {
-            if ($i != 9 && $i != 10 && $i != 13) {
-                $find = '_x' . sprintf('%04s', strtoupper(dechex($i))) . '_';
-                $replace = chr($i);
-                self::$controlCharacters[$find] = $replace;
-            }
-        }
-    }
-
-    /**
-     * Build SYLK characters array.
-     */
-    private static function buildSYLKCharacters(): void
-    {
-        self::$SYLKCharacters = [
-            "\x1B 0" => chr(0),
-            "\x1B 1" => chr(1),
-            "\x1B 2" => chr(2),
-            "\x1B 3" => chr(3),
-            "\x1B 4" => chr(4),
-            "\x1B 5" => chr(5),
-            "\x1B 6" => chr(6),
-            "\x1B 7" => chr(7),
-            "\x1B 8" => chr(8),
-            "\x1B 9" => chr(9),
-            "\x1B :" => chr(10),
-            "\x1B ;" => chr(11),
-            "\x1B <" => chr(12),
-            "\x1B =" => chr(13),
-            "\x1B >" => chr(14),
-            "\x1B ?" => chr(15),
-            "\x1B!0" => chr(16),
-            "\x1B!1" => chr(17),
-            "\x1B!2" => chr(18),
-            "\x1B!3" => chr(19),
-            "\x1B!4" => chr(20),
-            "\x1B!5" => chr(21),
-            "\x1B!6" => chr(22),
-            "\x1B!7" => chr(23),
-            "\x1B!8" => chr(24),
-            "\x1B!9" => chr(25),
-            "\x1B!:" => chr(26),
-            "\x1B!;" => chr(27),
-            "\x1B!<" => chr(28),
-            "\x1B!=" => chr(29),
-            "\x1B!>" => chr(30),
-            "\x1B!?" => chr(31),
-            "\x1B'?" => chr(127),
-            "\x1B(0" => '€', // 128 in CP1252
-            "\x1B(2" => '‚', // 130 in CP1252
-            "\x1B(3" => 'ƒ', // 131 in CP1252
-            "\x1B(4" => '„', // 132 in CP1252
-            "\x1B(5" => '…', // 133 in CP1252
-            "\x1B(6" => '†', // 134 in CP1252
-            "\x1B(7" => '‡', // 135 in CP1252
-            "\x1B(8" => 'ˆ', // 136 in CP1252
-            "\x1B(9" => '‰', // 137 in CP1252
-            "\x1B(:" => 'Š', // 138 in CP1252
-            "\x1B(;" => '‹', // 139 in CP1252
-            "\x1BNj" => 'Œ', // 140 in CP1252
-            "\x1B(>" => 'Ž', // 142 in CP1252
-            "\x1B)1" => '‘', // 145 in CP1252
-            "\x1B)2" => '’', // 146 in CP1252
-            "\x1B)3" => '“', // 147 in CP1252
-            "\x1B)4" => '”', // 148 in CP1252
-            "\x1B)5" => '•', // 149 in CP1252
-            "\x1B)6" => '–', // 150 in CP1252
-            "\x1B)7" => '—', // 151 in CP1252
-            "\x1B)8" => '˜', // 152 in CP1252
-            "\x1B)9" => '™', // 153 in CP1252
-            "\x1B):" => 'š', // 154 in CP1252
-            "\x1B);" => '›', // 155 in CP1252
-            "\x1BNz" => 'œ', // 156 in CP1252
-            "\x1B)>" => 'ž', // 158 in CP1252
-            "\x1B)?" => 'Ÿ', // 159 in CP1252
-            "\x1B*0" => ' ', // 160 in CP1252
-            "\x1BN!" => '¡', // 161 in CP1252
-            "\x1BN\"" => '¢', // 162 in CP1252
-            "\x1BN#" => '£', // 163 in CP1252
-            "\x1BN(" => '¤', // 164 in CP1252
-            "\x1BN%" => '¥', // 165 in CP1252
-            "\x1B*6" => '¦', // 166 in CP1252
-            "\x1BN'" => '§', // 167 in CP1252
-            "\x1BNH " => '¨', // 168 in CP1252
-            "\x1BNS" => '©', // 169 in CP1252
-            "\x1BNc" => 'ª', // 170 in CP1252
-            "\x1BN+" => '«', // 171 in CP1252
-            "\x1B*<" => '¬', // 172 in CP1252
-            "\x1B*=" => '­', // 173 in CP1252
-            "\x1BNR" => '®', // 174 in CP1252
-            "\x1B*?" => '¯', // 175 in CP1252
-            "\x1BN0" => '°', // 176 in CP1252
-            "\x1BN1" => '±', // 177 in CP1252
-            "\x1BN2" => '²', // 178 in CP1252
-            "\x1BN3" => '³', // 179 in CP1252
-            "\x1BNB " => '´', // 180 in CP1252
-            "\x1BN5" => 'µ', // 181 in CP1252
-            "\x1BN6" => '¶', // 182 in CP1252
-            "\x1BN7" => '·', // 183 in CP1252
-            "\x1B+8" => '¸', // 184 in CP1252
-            "\x1BNQ" => '¹', // 185 in CP1252
-            "\x1BNk" => 'º', // 186 in CP1252
-            "\x1BN;" => '»', // 187 in CP1252
-            "\x1BN<" => '¼', // 188 in CP1252
-            "\x1BN=" => '½', // 189 in CP1252
-            "\x1BN>" => '¾', // 190 in CP1252
-            "\x1BN?" => '¿', // 191 in CP1252
-            "\x1BNAA" => 'À', // 192 in CP1252
-            "\x1BNBA" => 'Á', // 193 in CP1252
-            "\x1BNCA" => 'Â', // 194 in CP1252
-            "\x1BNDA" => 'Ã', // 195 in CP1252
-            "\x1BNHA" => 'Ä', // 196 in CP1252
-            "\x1BNJA" => 'Å', // 197 in CP1252
-            "\x1BNa" => 'Æ', // 198 in CP1252
-            "\x1BNKC" => 'Ç', // 199 in CP1252
-            "\x1BNAE" => 'È', // 200 in CP1252
-            "\x1BNBE" => 'É', // 201 in CP1252
-            "\x1BNCE" => 'Ê', // 202 in CP1252
-            "\x1BNHE" => 'Ë', // 203 in CP1252
-            "\x1BNAI" => 'Ì', // 204 in CP1252
-            "\x1BNBI" => 'Í', // 205 in CP1252
-            "\x1BNCI" => 'Î', // 206 in CP1252
-            "\x1BNHI" => 'Ï', // 207 in CP1252
-            "\x1BNb" => 'Ð', // 208 in CP1252
-            "\x1BNDN" => 'Ñ', // 209 in CP1252
-            "\x1BNAO" => 'Ò', // 210 in CP1252
-            "\x1BNBO" => 'Ó', // 211 in CP1252
-            "\x1BNCO" => 'Ô', // 212 in CP1252
-            "\x1BNDO" => 'Õ', // 213 in CP1252
-            "\x1BNHO" => 'Ö', // 214 in CP1252
-            "\x1B-7" => '×', // 215 in CP1252
-            "\x1BNi" => 'Ø', // 216 in CP1252
-            "\x1BNAU" => 'Ù', // 217 in CP1252
-            "\x1BNBU" => 'Ú', // 218 in CP1252
-            "\x1BNCU" => 'Û', // 219 in CP1252
-            "\x1BNHU" => 'Ü', // 220 in CP1252
-            "\x1B-=" => 'Ý', // 221 in CP1252
-            "\x1BNl" => 'Þ', // 222 in CP1252
-            "\x1BN{" => 'ß', // 223 in CP1252
-            "\x1BNAa" => 'à', // 224 in CP1252
-            "\x1BNBa" => 'á', // 225 in CP1252
-            "\x1BNCa" => 'â', // 226 in CP1252
-            "\x1BNDa" => 'ã', // 227 in CP1252
-            "\x1BNHa" => 'ä', // 228 in CP1252
-            "\x1BNJa" => 'å', // 229 in CP1252
-            "\x1BNq" => 'æ', // 230 in CP1252
-            "\x1BNKc" => 'ç', // 231 in CP1252
-            "\x1BNAe" => 'è', // 232 in CP1252
-            "\x1BNBe" => 'é', // 233 in CP1252
-            "\x1BNCe" => 'ê', // 234 in CP1252
-            "\x1BNHe" => 'ë', // 235 in CP1252
-            "\x1BNAi" => 'ì', // 236 in CP1252
-            "\x1BNBi" => 'í', // 237 in CP1252
-            "\x1BNCi" => 'î', // 238 in CP1252
-            "\x1BNHi" => 'ï', // 239 in CP1252
-            "\x1BNs" => 'ð', // 240 in CP1252
-            "\x1BNDn" => 'ñ', // 241 in CP1252
-            "\x1BNAo" => 'ò', // 242 in CP1252
-            "\x1BNBo" => 'ó', // 243 in CP1252
-            "\x1BNCo" => 'ô', // 244 in CP1252
-            "\x1BNDo" => 'õ', // 245 in CP1252
-            "\x1BNHo" => 'ö', // 246 in CP1252
-            "\x1B/7" => '÷', // 247 in CP1252
-            "\x1BNy" => 'ø', // 248 in CP1252
-            "\x1BNAu" => 'ù', // 249 in CP1252
-            "\x1BNBu" => 'ú', // 250 in CP1252
-            "\x1BNCu" => 'û', // 251 in CP1252
-            "\x1BNHu" => 'ü', // 252 in CP1252
-            "\x1B/=" => 'ý', // 253 in CP1252
-            "\x1BN|" => 'þ', // 254 in CP1252
-            "\x1BNHy" => 'ÿ', // 255 in CP1252
-        ];
-    }
-
     /**
      * Get whether iconv extension is available.
      */
@@ -258,17 +296,6 @@ class StringHelper
         return self::$isIconvEnabled;
     }
 
-    private static function buildCharacterSets(): void
-    {
-        if (empty(self::$controlCharacters)) {
-            self::buildControlCharacters();
-        }
-
-        if (empty(self::$SYLKCharacters)) {
-            self::buildSYLKCharacters();
-        }
-    }
-
     /**
      * Convert from OpenXML escaped control character to PHP control character.
      *
@@ -284,9 +311,7 @@ class StringHelper
      */
     public static function controlCharacterOOXML2PHP(string $textValue): string
     {
-        self::buildCharacterSets();
-
-        return str_replace(array_keys(self::$controlCharacters), array_values(self::$controlCharacters), $textValue);
+        return str_replace(self::CONTROL_CHARACTERS_VALUES, self::CONTROL_CHARACTERS_KEYS, $textValue);
     }
 
     /**
@@ -304,9 +329,7 @@ class StringHelper
      */
     public static function controlCharacterPHP2OOXML(string $textValue): string
     {
-        self::buildCharacterSets();
-
-        return str_replace(array_values(self::$controlCharacters), array_keys(self::$controlCharacters), $textValue);
+        return str_replace(self::CONTROL_CHARACTERS_KEYS, self::CONTROL_CHARACTERS_VALUES, $textValue);
     }
 
     /**
@@ -619,14 +642,12 @@ class StringHelper
      */
     public static function SYLKtoUTF8(string $textValue): string
     {
-        self::buildCharacterSets();
-
         // If there is no escape character in the string there is nothing to do
-        if (!str_contains($textValue, '')) {
+        if (!str_contains($textValue, "\x1b")) {
             return $textValue;
         }
 
-        foreach (self::$SYLKCharacters as $k => $v) {
+        foreach (self::SYLK_CHARACTERS as $k => $v) {
             $textValue = str_replace($k, $v, $textValue);
         }
 


Test patch:
diff --git a/tests/PhpSpreadsheetTests/Shared/Issue347Test.php b/tests/PhpSpreadsheetTests/Shared/Issue347Test.php
new file mode 100644
index 000000000..c4e7707b6
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Shared/Issue347Test.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+namespace PhpOffice\PhpSpreadsheetTests\Shared;
+
+use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder;
+use PhpOffice\PhpSpreadsheet\Reader\BaseReader;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
+use PHPUnit\Framework\Attributes\DataProvider;
+
+class Issue347Test extends AbstractFunctional
+{
+    public function readerPreserveCr(BaseReader $reader): void
+    {
+        $binder = new DefaultValueBinder();
+        $binder->setPreserveCr(true);
+        $reader->setValueBinder($binder);
+    }
+
+    #[DataProvider('providerType')]
+    public function testPreserveCr(string $format): void
+    {
+        $s1 = "AB\r\nC\tD";
+        $spreadsheet = new Spreadsheet();
+        $binder = new DefaultValueBinder();
+        $binder->setPreserveCr(true);
+        $spreadsheet->setValueBinder($binder);
+        $sheet = $spreadsheet->getActiveSheet();
+        $sheet->getCell('A1')->setValue($s1);
+        $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format, readerCustomizer: $this->readerPreserveCr(...));
+        $spreadsheet->disconnectWorksheets();
+        $rsheet = $reloadedSpreadsheet->getActiveSheet();
+        $s2 = $rsheet->getCell('A1')->getValue();
+        self::assertSame($s1, $s2);
+        $reloadedSpreadsheet->disconnectWorksheets();
+    }
+
+    #[DataProvider('providerType')]
+    public function testNoPreserveCr(string $format): void
+    {
+        $s1 = "AB\r\nC\tD";
+        $spreadsheet = new Spreadsheet();
+        $sheet = $spreadsheet->getActiveSheet();
+        $sheet->getCell('A1')->setValue($s1);
+        $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format);
+        $spreadsheet->disconnectWorksheets();
+        $rsheet = $reloadedSpreadsheet->getActiveSheet();
+        $s2 = $rsheet->getCell('A1')->getValue();
+        self::assertNotEquals($s1, $s2);
+        self::assertSame("AB\nC\tD", $s2);
+        $reloadedSpreadsheet->disconnectWorksheets();
+    }
+
+    public static function providerType(): array
+    {
+        return [['Xls'], ['Xlsx'], ['Ods']];
+    }
+}