From 0be8c639a97bf78109d229e711f59bb958fc7cb3 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 7 Sep 2026 11:58:03 +1000 Subject: [PATCH] Tolerate empty OTL data when slicing a line of text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit applyOTL() resets $this->OTLdata to [] and returns early for a blank string, but MultiCell() takes that empty array as "there is OTL data here" and hands it to sliceOTLdata(), which reads 'group' and 'GPOSinfo' unconditionally. On PHP 8 that is two "Undefined array key" warnings plus a null-to-substr() deprecation, for every MultiCell() call with an empty or whitespace-only string. Guard the two reads the way 'char_data' is already guarded, rather than changing what applyOTL() leaves behind — ten call sites read $this->otl->OTLdata and most test it for truthiness. Mirrors mpdf/mpdf#2197 (mpdf/mpdf#2158). Co-Authored-By: Claude Opus 5 --- src/Otl.php | 5 +-- tests/Issues/Issue2158Test.php | 32 +++++++++++++++++ tests/Mpdf/OtlTest.php | 63 ++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 tests/Issues/Issue2158Test.php create mode 100644 tests/Mpdf/OtlTest.php diff --git a/src/Otl.php b/src/Otl.php index 3a458afac..6e3176850 100644 --- a/src/Otl.php +++ b/src/Otl.php @@ -5747,10 +5747,11 @@ public function splitOTLdata(&$cOTLdata, $OTLcutoffpos, $OTLrestartpos = '') public function sliceOTLdata($OTLdata, $pos, $len) { + // applyOTL() leaves OTLdata empty for a blank string, so every key here is optional $newOTLdata = ['GPOSinfo' => [], 'char_data' => []]; - $newOTLdata['group'] = substr($OTLdata['group'], $pos, $len); + $newOTLdata['group'] = isset($OTLdata['group']) ? substr($OTLdata['group'], $pos, $len) : ''; - if ($OTLdata['GPOSinfo']) { + if (!empty($OTLdata['GPOSinfo'])) { foreach ($OTLdata['GPOSinfo'] as $k => $val) { if ($k >= $pos && $k < ($pos + $len)) { $newOTLdata['GPOSinfo'][($k - $pos)] = $val; diff --git a/tests/Issues/Issue2158Test.php b/tests/Issues/Issue2158Test.php new file mode 100644 index 000000000..0c53564aa --- /dev/null +++ b/tests/Issues/Issue2158Test.php @@ -0,0 +1,32 @@ + 'dejavusans']); + $mpdf->AddPage(); + $mpdf->MultiCell(0, 5, ''); + + $output = $mpdf->OutputBinaryData(); + $this->assertStringStartsWith('%PDF-', $output); + + $mpdf->cleanup(); + } + + public function testMultiCellWithWhitespaceOnlyString() + { + $mpdf = new \Mpdf\Mpdf(['default_font' => 'dejavusans']); + $mpdf->AddPage(); + $mpdf->MultiCell(0, 5, " \n "); + + $output = $mpdf->OutputBinaryData(); + $this->assertStringStartsWith('%PDF-', $output); + + $mpdf->cleanup(); + } + +} diff --git a/tests/Mpdf/OtlTest.php b/tests/Mpdf/OtlTest.php new file mode 100644 index 000000000..3a2798f10 --- /dev/null +++ b/tests/Mpdf/OtlTest.php @@ -0,0 +1,63 @@ +mpdf = new Mpdf(['mode' => 'c']); + $this->otl = new Otl($this->mpdf, new FontCache(new Cache(sys_get_temp_dir() . '/mpdf-otl-test'))); + } + + protected function tear_down() + { + parent::tear_down(); + + $this->mpdf->cleanup(); + } + + public function testSliceOfPopulatedData() + { + $OTLdata = [ + 'group' => 'SCCSC', + 'GPOSinfo' => [1 => ['GPOSinfo'], 3 => ['other']], + 'char_data' => [['bidi_class' => 0], ['bidi_class' => 1], ['bidi_class' => 2], ['bidi_class' => 3], ['bidi_class' => 4]], + ]; + + $slice = $this->otl->sliceOTLdata($OTLdata, 1, 3); + + $this->assertSame('CCS', $slice['group']); + $this->assertSame([0 => ['GPOSinfo'], 2 => ['other']], $slice['GPOSinfo']); + $this->assertCount(3, $slice['char_data']); + } + + /** + * applyOTL() resets OTLdata to an empty array for a blank string, and MultiCell() slices + * whatever it is handed. See mpdf/mpdf#2158. + */ + public function testSliceOfEmptyDataReturnsAnEmptyStructure() + { + $slice = $this->otl->sliceOTLdata([], 0, 0); + + $this->assertSame('', $slice['group']); + $this->assertSame([], $slice['GPOSinfo']); + $this->assertSame([], $slice['char_data']); + } + +}