Mpdf Styling of Multi-Line Strings

Fix #4773. Mpdf is applying cell style only to the first line of a multi-line string. Explanation - Html Writer replaces newlines with `<br />` by calling `nl2br`, at least that was the intention. However, `nl2br` doesn't replace - it prepends. This doesn't do any harm in Html, Dompdf, or Tcpdf. However, Mpdf is known to be subject to occasional regexp backtracking errors when parsing large blocks of html. To avoid this, we chunk the html by splitting it at newlines, but this causes the styling to be lost for rich text elements following a newline.

The solution to this problem seems easy enough - replace `nl2br` with a routine that replaces rather than prepends. Unfortunately, Html Reader needs the newlines intact in order to parse things correctly. So a solution where the `nl2br` calls are used for non-Mpdf and using a substitute routine for Mpdf ought to work. However, doing my testing uncovered another bug - rich text with newlines can result in multiple `<br />` tags (see new Html test). The solution to this is a bit kludgey, but it does seem to fix both the Mpdf problem and the newly discovered Html problem. It will also tend to avoid the very minor problem of generating different line endings on Windows systems.
This commit is contained in:
oleibman
2026-01-07 21:34:34 -08:00
parent 0544868f90
commit f3f09bc9a6
4 changed files with 131 additions and 3 deletions
+18 -2
View File
@@ -57,6 +57,8 @@ class Html extends BaseWriter
*/
public const COMMENT_HTML_TAGS_PLAINTEXT = true;
private const BRX = '<br />';
/**
* Spreadsheet object.
*/
@@ -260,6 +262,11 @@ class Html extends BaseWriter
// Write footer
$html .= $this->generateHTMLFooter();
if ($this instanceof Pdf\Mpdf) {
$html = str_replace(self::BRX, '<br />', $html);
} else {
$html = str_replace(self::BRX, '<br />' . PHP_EOL, $html);
}
$callback = $this->editHtmlCallback;
if ($callback) {
$html = $callback($html);
@@ -1506,7 +1513,7 @@ class Html extends BaseWriter
}
}
return nl2br($cellData);
return self::nl2brx($cellData);
}
private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void
@@ -1573,7 +1580,7 @@ class Html extends BaseWriter
$cellData = Preg::replace('/(?m)(?:^|\G) /', '&nbsp;', $cellData);
// convert newline "\n" to '<br>'
$cellData = nl2br($cellData);
$cellData = self::nl2brx($cellData);
// Extend CSS class?
$dataType = $cell->getDataType();
@@ -2312,4 +2319,13 @@ class Html extends BaseWriter
return $this;
}
private static function nl2brx(string $string, bool $useXhtml = false): string
{
return str_replace(
["\r\n", "\n\r", "\r", "\n"],
self::BRX,
$string
);
}
}
@@ -100,7 +100,9 @@ class CommentAlignmentTest extends AbstractFunctional
$rsheet = $reloadedSpreadsheet->getActiveSheet();
$comment1 = $rsheet->getComment('A1');
self::assertSame($text, $comment1->getText()->getPlainText());
$textSplit = preg_split('/\r?\n/', $text);
$resultSplit = preg_split('/\r?\n/', $comment1->getText()->getPlainText());
self::assertSame($textSplit, $resultSplit);
$comment->setTextboxDirection(Comment::TEXTBOX_DIRECTION_RTL);
self::assertSame('right', $comment1->getAlignment());
self::assertSame('rtl', $comment1->getTextboxDirection());
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Writer\Html;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;
use PHPUnit\Framework\TestCase;
class Issue4773Test extends TestCase
{
public static function testLineBreaks(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getPageSetup()->setFitToWidth(1);
$sheet->getPageSetup()->setFitToHeight(0);
$sheet->getPageMargins()->setTop(0.2);
$sheet->getPageMargins()->setRight(0.2);
$sheet->getPageMargins()->setLeft(0.2);
$sheet->getPageMargins()->setBottom(0.5);
$sheet->getColumnDimension('A')->setWidth(20);
$sheet->getColumnDimension('B')->setWidth(20);
$sheet->getStyle('A1')->getFont()
->setSize(12)
->setItalic(true);
$sheet->getStyle('A1')->getAlignment()
->setWrapText(true);
$sheet->setCellValue('A1', "ABC\nDEF\nGHI");
$richText = new RichText();
$run1 = $richText->createTextRun("bold\n");
$run1->getFont()?->setBold(true);
$run3 = $richText->createTextRun('italic');
$run3->getFont()?->setItalic(true);
$sheet->getStyle('B1')->getAlignment()
->setVertical(Alignment::VERTICAL_CENTER)
->setWrapText(true);
$sheet->setCellValue('B1', $richText);
$writer = new HtmlWriter($spreadsheet);
$content = $writer->generateHtmlAll();
$expected1 = '<td class="column0 style1 s">ABC<br />' . PHP_EOL . 'DEF<br />' . PHP_EOL . 'GHI</td>';
self::assertStringContainsString($expected1, $content, 'br tags without newline in normal text');
$expected2 = '<td class="column1 style2 inlineStr"><span style="font-weight:bold; text-decoration:normal; font-style:normal; color:#000000; font-family:\'Calibri\'; font-size:11pt">bold<br />' . PHP_EOL . '</span><span style="font-weight:normal; text-decoration:normal; font-style:italic; color:#000000; font-family:\'Calibri\'; font-size:11pt">italic</span></td>';
self::assertStringContainsString($expected2, $content, 'only one br tag, plus newline, in rich text');
$spreadsheet->disconnectWorksheets();
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Writer\Mpdf;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf as MpdfWriter;
use PHPUnit\Framework\TestCase;
class Issue4773Test extends TestCase
{
public static function testLineBreaks(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getPageSetup()->setFitToWidth(1);
$sheet->getPageSetup()->setFitToHeight(0);
$sheet->getPageMargins()->setTop(0.2);
$sheet->getPageMargins()->setRight(0.2);
$sheet->getPageMargins()->setLeft(0.2);
$sheet->getPageMargins()->setBottom(0.5);
$sheet->getColumnDimension('A')->setWidth(20);
$sheet->getColumnDimension('B')->setWidth(20);
$sheet->getStyle('A1')->getFont()
->setSize(12)
->setItalic(true);
$sheet->getStyle('A1')->getAlignment()
->setWrapText(true);
$sheet->setCellValue('A1', "ABC\nDEF\nGHI");
$richText = new RichText();
$run1 = $richText->createTextRun("bold\n");
$run1->getFont()?->setBold(true);
$run3 = $richText->createTextRun('italic');
$run3->getFont()?->setItalic(true);
$sheet->getStyle('B1')->getAlignment()
->setVertical(Alignment::VERTICAL_CENTER)
->setWrapText(true);
$sheet->setCellValue('B1', $richText);
$writer = new MpdfWriter($spreadsheet);
$content = $writer->generateHtmlAll();
$expected1 = '<td class="column0 style1 s" style="width:105pt">ABC<br />DEF<br />GHI</td>';
self::assertStringContainsString($expected1, $content, 'br tags without newline in normal text');
$expected2 = '<td class="column1 style2 inlineStr" style="width:105pt"><span style="font-weight:bold; text-decoration:normal; font-style:normal; color:#000000; font-family:\'Calibri\'; font-size:11pt">bold<br /></span><span style="font-weight:normal; text-decoration:normal; font-style:italic; color:#000000; font-family:\'Calibri\'; font-size:11pt">italic</span></td>';
self::assertStringContainsString($expected2, $content, 'br tag without newline in rich text');
$spreadsheet->disconnectWorksheets();
}
}