From 39df9c3bcce10f4a907ad23502c4dcf9c38a644a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 29 Jul 2022 06:14:28 -0700 Subject: [PATCH 1/4] Fix Some Pdf Problems (#2960) * Fix Some Pdf Problems Fix #1747. No support for text rotation in Pdf. That issue actually has a decent workaround, but PhpSpreadsheet should handle it on its own. Mpdf requires the proprietary text-rotate css attribute; Html and Dompdf will use the CSS3 attribute transform:rotate. Fix #1713. Some paper-size values in PhpSpreadsheet are strings, some are 2-element float arrays. Dompdf accepts strings or 4-element float arrays, where the first 2 elements are always 0. Convert the PhpSpreadsheet array accordingly before passing it to Dompdf. Some tests had been disabled when Dompdf and Tcpdf were slow to achieve PHP8 compliance. They achieved it some time ago. Re-enable the tests. * Remove Tcpdf From One Test No problem with the other tests I added it in for. --- samples/Basic/26_Utf8.php | 10 ++- samples/Pdf/21b_Pdf.php | 24 +++---- src/PhpSpreadsheet/Writer/Html.php | 15 +++++ src/PhpSpreadsheet/Writer/Pdf/Dompdf.php | 3 + src/PhpSpreadsheet/Writer/Pdf/Mpdf.php | 3 + .../Functional/StreamTest.php | 3 +- .../Writer/Dompdf/PaperSizeArrayTest.php | 65 +++++++++++++++++++ .../Writer/Dompdf/TextRotationTest.php | 24 +++++++ .../Writer/Html/TextRotationTest.php | 24 +++++++ .../Writer/Mpdf/TextRotationTest.php | 24 +++++++ 10 files changed, 174 insertions(+), 21 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Mpdf/TextRotationTest.php diff --git a/samples/Basic/26_Utf8.php b/samples/Basic/26_Utf8.php index 52953251..52a64509 100644 --- a/samples/Basic/26_Utf8.php +++ b/samples/Basic/26_Utf8.php @@ -12,12 +12,10 @@ $spreadsheet = $reader->load(__DIR__ . '/../templates/26template.xlsx'); // at this point, we could do some manipulations with the template, but we skip this step $helper->write($spreadsheet, __FILE__, ['Xlsx', 'Xls', 'Html']); -if (\PHP_VERSION_ID < 80000) { - // Export to PDF (.pdf) - $helper->log('Write to PDF format'); - IOFactory::registerWriter('Pdf', \PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf::class); - $helper->write($spreadsheet, __FILE__, ['Pdf']); -} +// Export to PDF (.pdf) +$helper->log('Write to PDF format'); +IOFactory::registerWriter('Pdf', \PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf::class); +$helper->write($spreadsheet, __FILE__, ['Pdf']); // Remove first two rows with field headers before exporting to CSV $helper->log('Removing first two heading rows for CSV export'); diff --git a/samples/Pdf/21b_Pdf.php b/samples/Pdf/21b_Pdf.php index ad2f609b..c67ff3d2 100644 --- a/samples/Pdf/21b_Pdf.php +++ b/samples/Pdf/21b_Pdf.php @@ -32,13 +32,11 @@ $spreadsheet->getActiveSheet()->setShowGridLines(false); $helper->log('Set orientation to landscape'); $spreadsheet->getActiveSheet()->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); -if (\PHP_VERSION_ID < 80000) { - $helper->log('Write to Dompdf'); - $writer = new Dompdf($spreadsheet); - $filename = $helper->getFileName('21b_Pdf_dompdf.xlsx', 'pdf'); - $writer->setEditHtmlCallback('replaceBody'); - $writer->save($filename); -} +$helper->log('Write to Dompdf'); +$writer = new Dompdf($spreadsheet); +$filename = $helper->getFileName('21b_Pdf_dompdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('replaceBody'); +$writer->save($filename); $helper->log('Write to Mpdf'); $writer = new Mpdf($spreadsheet); @@ -46,10 +44,8 @@ $filename = $helper->getFileName('21b_Pdf_mpdf.xlsx', 'pdf'); $writer->setEditHtmlCallback('replaceBody'); $writer->save($filename); -if (\PHP_VERSION_ID < 80000) { - $helper->log('Write to Tcpdf'); - $writer = new Tcpdf($spreadsheet); - $filename = $helper->getFileName('21b_Pdf_tcpdf.xlsx', 'pdf'); - $writer->setEditHtmlCallback('replaceBody'); - $writer->save($filename); -} +$helper->log('Write to Tcpdf'); +$writer = new Tcpdf($spreadsheet); +$filename = $helper->getFileName('21b_Pdf_tcpdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('replaceBody'); +$writer->save($filename); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index da32025a..6e51b7a8 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -126,6 +126,13 @@ class Html extends BaseWriter */ protected $isPdf = false; + /** + * Is the current writer creating mPDF? + * + * @var bool + */ + protected $isMPdf = false; + /** * Generate the Navigation block. * @@ -1003,6 +1010,14 @@ class Html extends BaseWriter $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px'; } } + $rotation = $alignment->getTextRotation(); + if ($rotation !== 0 && $rotation !== Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) { + if ($this->isMPdf) { + $css['text-rotate'] = "$rotation"; + } else { + $css['transform'] = "rotate({$rotation}deg)"; + } + } return $css; } diff --git a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index fc96f904..bf9e28cb 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -35,6 +35,9 @@ class Dompdf extends Pdf $orientation = ($orientation === PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; $printPaperSize = $this->getPaperSize() ?? $setup->getPaperSize(); $paperSize = self::$paperSizes[$printPaperSize] ?? PageSetup::getPaperSizeDefault(); + if (is_array($paperSize) && count($paperSize) === 2) { + $paperSize = [0.0, 0.0, $paperSize[0], $paperSize[1]]; + } $orientation = ($orientation == 'L') ? 'landscape' : 'portrait'; diff --git a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php index 281e1a4f..d0ce9ed4 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php @@ -8,6 +8,9 @@ use PhpOffice\PhpSpreadsheet\Writer\Pdf; class Mpdf extends Pdf { + /** @var bool */ + protected $isMPdf = true; + /** * Gets the implementation of external PDF library that should be used. * diff --git a/tests/PhpSpreadsheetTests/Functional/StreamTest.php b/tests/PhpSpreadsheetTests/Functional/StreamTest.php index 3911aaa6..a84a2490 100644 --- a/tests/PhpSpreadsheetTests/Functional/StreamTest.php +++ b/tests/PhpSpreadsheetTests/Functional/StreamTest.php @@ -17,12 +17,13 @@ class StreamTest extends TestCase ['Csv'], ['Html'], ['Mpdf'], + ['Dompdf'], ]; if (\PHP_VERSION_ID < 80000) { $providerFormats = array_merge( $providerFormats, - [['Tcpdf'], ['Dompdf']] + [['Tcpdf']] ); } diff --git a/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php b/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php new file mode 100644 index 00000000..815d7e3d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php @@ -0,0 +1,65 @@ +outfile !== '') { + unlink($this->outfile); + $this->outfile = ''; + } + } + + public function testPaperSizeArray(): void + { + // Issue 1713 - array in PhpSpreadsheet is 2 elements, + // but in Dompdf it is 4 elements, first 2 are zero. + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + // TABLOID is a 2-element array in Writer/Pdf.php $paperSizes + $size = PageSetup::PAPERSIZE_TABLOID; + $sheet->getPageSetup()->setPaperSize($size); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Dompdf($spreadsheet); + $this->outfile = File::temporaryFilename(); + $writer->save($this->outfile); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + $contents = file_get_contents($this->outfile); + self::assertNotFalse($contents); + self::assertStringContainsString('/MediaBox [0.000 0.000 792.000 1224.000]', $contents); + } + + public function testPaperSizeNotArray(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + // LETTER is a string in Writer/Pdf.php $paperSizes + $size = PageSetup::PAPERSIZE_LETTER; + $sheet->getPageSetup()->setPaperSize($size); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Dompdf($spreadsheet); + $this->outfile = File::temporaryFilename(); + $writer->save($this->outfile); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + $contents = file_get_contents($this->outfile); + self::assertNotFalse($contents); + self::assertStringContainsString('/MediaBox [0.000 0.000 612.000 792.000]', $contents); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php b/tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php new file mode 100644 index 00000000..2aa9d5ed --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php @@ -0,0 +1,24 @@ +getActiveSheet(); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Dompdf($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString(' transform:rotate(90deg);', $html); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php b/tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php new file mode 100644 index 00000000..da928457 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php @@ -0,0 +1,24 @@ +getActiveSheet(); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Html($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString(' transform:rotate(90deg);', $html); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Mpdf/TextRotationTest.php b/tests/PhpSpreadsheetTests/Writer/Mpdf/TextRotationTest.php new file mode 100644 index 00000000..000a33b4 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Mpdf/TextRotationTest.php @@ -0,0 +1,24 @@ +getActiveSheet(); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Mpdf($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString(' text-rotate:90;', $html); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + } +} From 641b6d0ccb8aa3f5265a2382db19c8c81d5eb1be Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 29 Jul 2022 07:11:37 -0700 Subject: [PATCH 2/4] Improve Coverage for Shared/Font (#2961) Shared/Font is hardly covered in unit tests (as opposed to Style/Font which is completely covered). And it presented some good opportunities for code optimization. I wrote and tested the new unit tests first, then optimized the code and confirmed that everything still works. There is still a bit of a gap with "exact" measurements. I had tests ready, but had to withdraw them when I discovered they weren't quite portable (see https://github.com/php/php-src/issues/9073). --- phpstan-baseline.neon | 45 -- src/PhpSpreadsheet/Shared/Font.php | 538 +++++++----------- .../Reader/Xls/Rc4Test.php | 21 + .../PhpSpreadsheetTests/Shared/Font2Test.php | 149 +++++ .../PhpSpreadsheetTests/Shared/Font3Test.php | 53 ++ 5 files changed, 424 insertions(+), 382 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php create mode 100644 tests/PhpSpreadsheetTests/Shared/Font2Test.php create mode 100644 tests/PhpSpreadsheetTests/Shared/Font3Test.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 024f1fbd..49ab167e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2270,51 +2270,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php - - - message: "#^Cannot access offset 0 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Cannot access offset 2 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Cannot access offset 4 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Cannot access offset 6 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Parameter \\#1 \\$size of function imagettfbbox expects float, float\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Parameter \\#2 \\$defaultFont of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Drawing\\:\\:pixelsToCellDimension\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Font\\:\\:\\$autoSizeMethods has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Variable \\$cellText on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\EigenvalueDecomposition\\:\\:\\$cdivi has no type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index 1adf213e..33796b4c 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -13,7 +13,7 @@ class Font const AUTOSIZE_METHOD_APPROX = 'approx'; const AUTOSIZE_METHOD_EXACT = 'exact'; - private static $autoSizeMethods = [ + private const AUTOSIZE_METHODS = [ self::AUTOSIZE_METHOD_APPROX, self::AUTOSIZE_METHOD_EXACT, ]; @@ -101,6 +101,105 @@ class Font const VERDANA_ITALIC = 'verdanai.ttf'; const VERDANA_BOLD_ITALIC = 'verdanaz.ttf'; + const FONT_FILE_NAMES = [ + 'Arial' => [ + 'x' => self::ARIAL, + 'xb' => self::ARIAL_BOLD, + 'xi' => self::ARIAL_ITALIC, + 'xbi' => self::ARIAL_BOLD_ITALIC, + ], + 'Calibri' => [ + 'x' => self::CALIBRI, + 'xb' => self::CALIBRI_BOLD, + 'xi' => self::CALIBRI_ITALIC, + 'xbi' => self::CALIBRI_BOLD_ITALIC, + ], + 'Comic Sans MS' => [ + 'x' => self::COMIC_SANS_MS, + 'xb' => self::COMIC_SANS_MS_BOLD, + 'xi' => self::COMIC_SANS_MS, + 'xbi' => self::COMIC_SANS_MS_BOLD, + ], + 'Courier New' => [ + 'x' => self::COURIER_NEW, + 'xb' => self::COURIER_NEW_BOLD, + 'xi' => self::COURIER_NEW_ITALIC, + 'xbi' => self::COURIER_NEW_BOLD_ITALIC, + ], + 'Georgia' => [ + 'x' => self::GEORGIA, + 'xb' => self::GEORGIA_BOLD, + 'xi' => self::GEORGIA_ITALIC, + 'xbi' => self::GEORGIA_BOLD_ITALIC, + ], + 'Impact' => [ + 'x' => self::IMPACT, + 'xb' => self::IMPACT, + 'xi' => self::IMPACT, + 'xbi' => self::IMPACT, + ], + 'Liberation Sans' => [ + 'x' => self::LIBERATION_SANS, + 'xb' => self::LIBERATION_SANS_BOLD, + 'xi' => self::LIBERATION_SANS_ITALIC, + 'xbi' => self::LIBERATION_SANS_BOLD_ITALIC, + ], + 'Lucida Console' => [ + 'x' => self::LUCIDA_CONSOLE, + 'xb' => self::LUCIDA_CONSOLE, + 'xi' => self::LUCIDA_CONSOLE, + 'xbi' => self::LUCIDA_CONSOLE, + ], + 'Lucida Sans Unicode' => [ + 'x' => self::LUCIDA_SANS_UNICODE, + 'xb' => self::LUCIDA_SANS_UNICODE, + 'xi' => self::LUCIDA_SANS_UNICODE, + 'xbi' => self::LUCIDA_SANS_UNICODE, + ], + 'Microsoft Sans Serif' => [ + 'x' => self::MICROSOFT_SANS_SERIF, + 'xb' => self::MICROSOFT_SANS_SERIF, + 'xi' => self::MICROSOFT_SANS_SERIF, + 'xbi' => self::MICROSOFT_SANS_SERIF, + ], + 'Palatino Linotype' => [ + 'x' => self::PALATINO_LINOTYPE, + 'xb' => self::PALATINO_LINOTYPE_BOLD, + 'xi' => self::PALATINO_LINOTYPE_ITALIC, + 'xbi' => self::PALATINO_LINOTYPE_BOLD_ITALIC, + ], + 'Symbol' => [ + 'x' => self::SYMBOL, + 'xb' => self::SYMBOL, + 'xi' => self::SYMBOL, + 'xbi' => self::SYMBOL, + ], + 'Tahoma' => [ + 'x' => self::TAHOMA, + 'xb' => self::TAHOMA_BOLD, + 'xi' => self::TAHOMA, + 'xbi' => self::TAHOMA_BOLD, + ], + 'Times New Roman' => [ + 'x' => self::TIMES_NEW_ROMAN, + 'xb' => self::TIMES_NEW_ROMAN_BOLD, + 'xi' => self::TIMES_NEW_ROMAN_ITALIC, + 'xbi' => self::TIMES_NEW_ROMAN_BOLD_ITALIC, + ], + 'Trebuchet MS' => [ + 'x' => self::TREBUCHET_MS, + 'xb' => self::TREBUCHET_MS_BOLD, + 'xi' => self::TREBUCHET_MS_ITALIC, + 'xbi' => self::TREBUCHET_MS_BOLD_ITALIC, + ], + 'Verdana' => [ + 'x' => self::VERDANA, + 'xb' => self::VERDANA_BOLD, + 'xi' => self::VERDANA_ITALIC, + 'xbi' => self::VERDANA_BOLD_ITALIC, + ], + ]; + /** * AutoSize method. * @@ -113,54 +212,65 @@ class Font * * @var string */ - private static $trueTypeFontPath; + private static $trueTypeFontPath = ''; /** * How wide is a default column for a given default font and size? * Empirical data found by inspecting real Excel files and reading off the pixel width * in Microsoft Office Excel 2007. + * Added height in points. + */ + public const DEFAULT_COLUMN_WIDTHS = [ + 'Arial' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0], + + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25], + 9 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.0], + 10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75], + ], + 'Calibri' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.00], + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25], + 9 => ['px' => 56, 'width' => 9.33203125, 'height' => 12.0], + 10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75], + 11 => ['px' => 64, 'width' => 9.14062500, 'height' => 15.0], + ], + 'Verdana' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0], + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 64, 'width' => 9.14062500, 'height' => 10.5], + 9 => ['px' => 72, 'width' => 9.00000000, 'height' => 11.25], + 10 => ['px' => 72, 'width' => 9.00000000, 'height' => 12.75], + ], + ]; + + /** + * List of column widths. Replaced by constant; + * previously it was public and updateable, allowing + * user to make inappropriate alterations. + * + * @deprecated 1.25.0 Use DEFAULT_COLUMN_WIDTHS constant instead. * * @var array */ - public static $defaultColumnWidths = [ - 'Arial' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 56, 'width' => 9.33203125], - 9 => ['px' => 64, 'width' => 9.14062500], - 10 => ['px' => 64, 'width' => 9.14062500], - ], - 'Calibri' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 56, 'width' => 9.33203125], - 9 => ['px' => 56, 'width' => 9.33203125], - 10 => ['px' => 64, 'width' => 9.14062500], - 11 => ['px' => 64, 'width' => 9.14062500], - ], - 'Verdana' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 64, 'width' => 9.14062500], - 9 => ['px' => 72, 'width' => 9.00000000], - 10 => ['px' => 72, 'width' => 9.00000000], - ], - ]; + public static $defaultColumnWidths = self::DEFAULT_COLUMN_WIDTHS; /** * Set autoSize method. @@ -171,7 +281,7 @@ class Font */ public static function setAutoSizeMethod($method) { - if (!in_array($method, self::$autoSizeMethods)) { + if (!in_array($method, self::AUTOSIZE_METHODS)) { return false; } self::$autoSizeMethod = $method; @@ -219,7 +329,7 @@ class Font * Calculate an (approximate) OpenXML column width, based on font size and text contained. * * @param FontStyle $font Font object - * @param RichText|string $cellText Text to calculate width + * @param null|RichText|string $cellText Text to calculate width * @param int $rotation Rotation angle * @param null|FontStyle $defaultFont Font object * @param bool $filterAdjustment Add space for Autofilter or Table dropdown @@ -238,7 +348,8 @@ class Font } // Special case if there are one or more newline characters ("\n") - if (strpos($cellText ?? '', "\n") !== false) { + $cellText = $cellText ?? ''; + if (strpos($cellText, "\n") !== false) { $lineTexts = explode("\n", $cellText); $lineWidths = []; foreach ($lineTexts as $lineText) { @@ -281,7 +392,7 @@ class Font } // Convert from pixel width to column width - $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont); + $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont ?? new FontStyle()); // Return return (int) round($columnWidth, 6); @@ -299,7 +410,12 @@ class Font // font size should really be supplied in pixels in GD2, // but since GD2 seems to assume 72dpi, pixels and points are the same $fontFile = self::getTrueTypeFontFileFromFont($font); - $textBox = imagettfbbox($font->getSize(), $rotation, $fontFile, $text); + $textBox = imagettfbbox($font->getSize() ?? 10.0, $rotation, $fontFile, $text); + if ($textBox === false) { + // @codeCoverageIgnoreStart + throw new PhpSpreadsheetException('imagettfbbox failed'); + // @codeCoverageIgnoreEnd + } // Get corners positions $lowerLeftCornerX = $textBox[0]; @@ -409,129 +525,48 @@ class Font * * @return string Path to TrueType font file */ - public static function getTrueTypeFontFileFromFont(FontStyle $font) + public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkPath = true) { - if (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath)) { + if ($checkPath && (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath))) { throw new PhpSpreadsheetException('Valid directory to TrueType Font files not specified'); } $name = $font->getName(); + if (!isset(self::FONT_FILE_NAMES[$name])) { + throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); + } $bold = $font->getBold(); $italic = $font->getItalic(); - - // Check if we can map font to true type font file - switch ($name) { - case 'Arial': - $fontFile = ( - $bold ? ($italic ? self::ARIAL_BOLD_ITALIC : self::ARIAL_BOLD) - : ($italic ? self::ARIAL_ITALIC : self::ARIAL) - ); - - break; - case 'Calibri': - $fontFile = ( - $bold ? ($italic ? self::CALIBRI_BOLD_ITALIC : self::CALIBRI_BOLD) - : ($italic ? self::CALIBRI_ITALIC : self::CALIBRI) - ); - - break; - case 'Courier New': - $fontFile = ( - $bold ? ($italic ? self::COURIER_NEW_BOLD_ITALIC : self::COURIER_NEW_BOLD) - : ($italic ? self::COURIER_NEW_ITALIC : self::COURIER_NEW) - ); - - break; - case 'Comic Sans MS': - $fontFile = ( - $bold ? self::COMIC_SANS_MS_BOLD : self::COMIC_SANS_MS - ); - - break; - case 'Georgia': - $fontFile = ( - $bold ? ($italic ? self::GEORGIA_BOLD_ITALIC : self::GEORGIA_BOLD) - : ($italic ? self::GEORGIA_ITALIC : self::GEORGIA) - ); - - break; - case 'Impact': - $fontFile = self::IMPACT; - - break; - case 'Liberation Sans': - $fontFile = ( - $bold ? ($italic ? self::LIBERATION_SANS_BOLD_ITALIC : self::LIBERATION_SANS_BOLD) - : ($italic ? self::LIBERATION_SANS_ITALIC : self::LIBERATION_SANS) - ); - - break; - case 'Lucida Console': - $fontFile = self::LUCIDA_CONSOLE; - - break; - case 'Lucida Sans Unicode': - $fontFile = self::LUCIDA_SANS_UNICODE; - - break; - case 'Microsoft Sans Serif': - $fontFile = self::MICROSOFT_SANS_SERIF; - - break; - case 'Palatino Linotype': - $fontFile = ( - $bold ? ($italic ? self::PALATINO_LINOTYPE_BOLD_ITALIC : self::PALATINO_LINOTYPE_BOLD) - : ($italic ? self::PALATINO_LINOTYPE_ITALIC : self::PALATINO_LINOTYPE) - ); - - break; - case 'Symbol': - $fontFile = self::SYMBOL; - - break; - case 'Tahoma': - $fontFile = ( - $bold ? self::TAHOMA_BOLD : self::TAHOMA - ); - - break; - case 'Times New Roman': - $fontFile = ( - $bold ? ($italic ? self::TIMES_NEW_ROMAN_BOLD_ITALIC : self::TIMES_NEW_ROMAN_BOLD) - : ($italic ? self::TIMES_NEW_ROMAN_ITALIC : self::TIMES_NEW_ROMAN) - ); - - break; - case 'Trebuchet MS': - $fontFile = ( - $bold ? ($italic ? self::TREBUCHET_MS_BOLD_ITALIC : self::TREBUCHET_MS_BOLD) - : ($italic ? self::TREBUCHET_MS_ITALIC : self::TREBUCHET_MS) - ); - - break; - case 'Verdana': - $fontFile = ( - $bold ? ($italic ? self::VERDANA_BOLD_ITALIC : self::VERDANA_BOLD) - : ($italic ? self::VERDANA_ITALIC : self::VERDANA) - ); - - break; - default: - throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); - - break; + $index = 'x'; + if ($bold) { + $index .= 'b'; } + if ($italic) { + $index .= 'i'; + } + $fontFile = self::FONT_FILE_NAMES[$name][$index]; - $fontFile = self::$trueTypeFontPath . $fontFile; + $separator = ''; + if (mb_strlen(self::$trueTypeFontPath) > 1 && mb_substr(self::$trueTypeFontPath, -1) !== '/' && mb_substr(self::$trueTypeFontPath, -1) !== '\\') { + $separator = DIRECTORY_SEPARATOR; + } + $fontFile = self::$trueTypeFontPath . $separator . $fontFile; // Check if file actually exists - if (!file_exists($fontFile)) { + if ($checkPath && !file_exists($fontFile)) { throw new PhpSpreadsheetException('TrueType Font file not found'); } return $fontFile; } + public const CHARSET_FROM_FONT_NAME = [ + 'EucrosiaUPC' => self::CHARSET_ANSI_THAI, + 'Wingdings' => self::CHARSET_SYMBOL, + 'Wingdings 2' => self::CHARSET_SYMBOL, + 'Wingdings 3' => self::CHARSET_SYMBOL, + ]; + /** * Returns the associated charset for the font name. * @@ -541,19 +576,7 @@ class Font */ public static function getCharsetFromFontName($fontName) { - switch ($fontName) { - // Add more cases. Check FONT records in real Excel files. - case 'EucrosiaUPC': - return self::CHARSET_ANSI_THAI; - case 'Wingdings': - return self::CHARSET_SYMBOL; - case 'Wingdings 2': - return self::CHARSET_SYMBOL; - case 'Wingdings 3': - return self::CHARSET_SYMBOL; - default: - return self::CHARSET_ANSI_LATIN; - } + return self::CHARSET_FROM_FONT_NAME[$fontName] ?? self::CHARSET_ANSI_LATIN; } /** @@ -567,17 +590,17 @@ class Font */ public static function getDefaultColumnWidthByFont(FontStyle $font, $returnAsPixels = false) { - if (isset(self::$defaultColumnWidths[$font->getName()][$font->getSize()])) { + if (isset(self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()])) { // Exact width can be determined $columnWidth = $returnAsPixels ? - self::$defaultColumnWidths[$font->getName()][$font->getSize()]['px'] - : self::$defaultColumnWidths[$font->getName()][$font->getSize()]['width']; + self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['px'] + : self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['width']; } else { // We don't have data for this particular font and size, use approximation by // extrapolating from Calibri 11 $columnWidth = $returnAsPixels ? - self::$defaultColumnWidths['Calibri'][11]['px'] - : self::$defaultColumnWidths['Calibri'][11]['width']; + self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['px'] + : self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['width']; $columnWidth = $columnWidth * $font->getSize() / 11; // Round pixels to closest integer @@ -599,173 +622,14 @@ class Font */ public static function getDefaultRowHeightByFont(FontStyle $font) { - switch ($font->getName()) { - case 'Arial': - switch ($font->getSize()) { - case 10: - // inspection of Arial 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Arial 9 workbook says 12.00pt ~16px - $rowHeight = 12; - - break; - case 8: - // inspection of Arial 8 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 7: - // inspection of Arial 7 workbook says 9.00pt ~12px - $rowHeight = 9; - - break; - case 6: - case 5: - // inspection of Arial 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Arial 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Arial 3 workbook says 6.00pt ~8px - $rowHeight = 6; - - break; - case 2: - case 1: - // inspection of Arial 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Arial 10 workbook as an approximation, extrapolation - $rowHeight = 12.75 * $font->getSize() / 10; - - break; - } - - break; - case 'Calibri': - switch ($font->getSize()) { - case 11: - // inspection of Calibri 11 workbook says 15.00pt ~20px - $rowHeight = 15; - - break; - case 10: - // inspection of Calibri 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Calibri 9 workbook says 12.00pt ~16px - $rowHeight = 12; - - break; - case 8: - // inspection of Calibri 8 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 7: - // inspection of Calibri 7 workbook says 9.00pt ~12px - $rowHeight = 9; - - break; - case 6: - case 5: - // inspection of Calibri 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Calibri 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Calibri 3 workbook says 6.00pt ~8px - $rowHeight = 6.00; - - break; - case 2: - case 1: - // inspection of Calibri 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Calibri 11 workbook as an approximation, extrapolation - $rowHeight = 15 * $font->getSize() / 11; - - break; - } - - break; - case 'Verdana': - switch ($font->getSize()) { - case 10: - // inspection of Verdana 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Verdana 9 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 8: - // inspection of Verdana 8 workbook says 10.50pt ~14px - $rowHeight = 10.50; - - break; - case 7: - // inspection of Verdana 7 workbook says 9.00pt ~12px - $rowHeight = 9.00; - - break; - case 6: - case 5: - // inspection of Verdana 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Verdana 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Verdana 3 workbook says 6.00pt ~8px - $rowHeight = 6; - - break; - case 2: - case 1: - // inspection of Verdana 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Verdana 10 workbook as an approximation, extrapolation - $rowHeight = 12.75 * $font->getSize() / 10; - - break; - } - - break; - default: - // just use Calibri as an approximation - $rowHeight = 15 * $font->getSize() / 11; - - break; + $name = $font->getName(); + $size = $font->getSize(); + if (isset(self::DEFAULT_COLUMN_WIDTHS[$name][$size])) { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][$size]['height']; + } elseif ($name === 'Arial' || $name === 'Verdana') { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][10]['height'] * $size / 10.0; + } else { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['height'] * $size / 11.0; } return $rowHeight; diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php b/tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php new file mode 100644 index 00000000..c3056c08 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php @@ -0,0 +1,21 @@ +RC4($string)); + $expectedResult = '2ac2fecdd8fbb84638e3a4820eb205cc8e29c28b9d5d6b2ef974f311964971c90e8b9ca16467ef2dc6fc3520'; + self::assertSame($expectedResult, $result); + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/Font2Test.php b/tests/PhpSpreadsheetTests/Shared/Font2Test.php new file mode 100644 index 00000000..4bb247cf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Font2Test.php @@ -0,0 +1,149 @@ +providerCharsetFromFontName(); + foreach ($tests as $test) { + $thisTest = $test[0]; + if (array_key_exists($thisTest, $covered)) { + $covered[$thisTest] = 1; + } else { + $defaultCovered = true; + } + } + foreach ($covered as $key => $val) { + self::assertEquals(1, $val, "FontName $key not tested"); + } + self::assertTrue($defaultCovered, 'Default key not tested'); + } + + public function providerCharsetFromFontName(): array + { + return [ + ['EucrosiaUPC', Font::CHARSET_ANSI_THAI], + ['Wingdings', Font::CHARSET_SYMBOL], + ['Wingdings 2', Font::CHARSET_SYMBOL], + ['Wingdings 3', Font::CHARSET_SYMBOL], + ['Default', Font::CHARSET_ANSI_LATIN], + ]; + } + + public function testColumnWidths(): void + { + $widths = Font::DEFAULT_COLUMN_WIDTHS; + $fontNames = ['Arial', 'Calibri', 'Verdana']; + $font = new StyleFont(); + foreach ($fontNames as $fontName) { + $font->setName($fontName); + $array = $widths[$fontName]; + foreach ($array as $points => $array2) { + $font->setSize($points); + $px = $array2['px']; + $width = $array2['width']; + self::assertEquals($px, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals($width, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + } + } + $pxCalibri11 = $widths['Calibri'][11]['px']; + $widthCalibri11 = $widths['Calibri'][11]['width']; + $fontName = 'unknown'; + $points = 11; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals($pxCalibri11, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals($widthCalibri11, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + $points = 22; + $font->setSize($points); + self::assertEquals(2 * $pxCalibri11, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals(2 * $widthCalibri11, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + $fontName = 'Arial'; + $points = 33; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(3 * $pxCalibri11, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals(3 * $widthCalibri11, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + } + + public function testRowHeights(): void + { + $heights = Font::DEFAULT_COLUMN_WIDTHS; + $fontNames = ['Arial', 'Calibri', 'Verdana']; + $font = new StyleFont(); + foreach ($fontNames as $fontName) { + $font->setName($fontName); + $array = $heights[$fontName]; + foreach ($array as $points => $array2) { + $font->setSize($points); + $height = $array2['height']; + self::assertEquals($height, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + } + } + $heightArial10 = $heights['Arial'][10]['height']; + $fontName = 'Arial'; + $points = 20; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(2 * $heightArial10, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + $heightVerdana10 = $heights['Verdana'][10]['height']; + $fontName = 'Verdana'; + $points = 30; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(3 * $heightVerdana10, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + $heightCalibri11 = $heights['Calibri'][11]['height']; + $fontName = 'Calibri'; + $points = 22; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(2 * $heightCalibri11, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + $fontName = 'unknown'; + $points = 33; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(3 * $heightCalibri11, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + } + + public function testGetTrueTypeFontFileFromFont(): void + { + $fileNames = Font::FONT_FILE_NAMES; + $font = new StyleFont(); + foreach ($fileNames as $fontName => $fontNameArray) { + $font->setName($fontName); + $font->setBold(false); + $font->setItalic(false); + self::assertSame($fileNames[$fontName]['x'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName not bold not italic"); + $font->setBold(true); + $font->setItalic(false); + self::assertSame($fileNames[$fontName]['xb'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName bold not italic"); + $font->setBold(false); + $font->setItalic(true); + self::assertSame($fileNames[$fontName]['xi'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName not bold italic"); + $font->setBold(true); + $font->setItalic(true); + self::assertSame($fileNames[$fontName]['xbi'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName bold italic"); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/Font3Test.php b/tests/PhpSpreadsheetTests/Shared/Font3Test.php new file mode 100644 index 00000000..e91e7acf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Font3Test.php @@ -0,0 +1,53 @@ +holdDirectory = Font::getTrueTypeFontPath(); + } + + protected function tearDown(): void + { + Font::setTrueTypeFontPath($this->holdDirectory); + } + + public function testGetTrueTypeException1(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Valid directory to TrueType Font files not specified'); + $font = new StyleFont(); + $font->setName('unknown'); + Font::getTrueTypeFontFileFromFont($font); + } + + public function testGetTrueTypeException2(): void + { + Font::setTrueTypeFontPath(__DIR__); + $this->expectException(SSException::class); + $this->expectExceptionMessage('Unknown font name'); + $font = new StyleFont(); + $font->setName('unknown'); + Font::getTrueTypeFontFileFromFont($font); + } + + public function testGetTrueTypeException3(): void + { + Font::setTrueTypeFontPath(__DIR__); + $this->expectException(SSException::class); + $this->expectExceptionMessage('TrueType Font file not found'); + $font = new StyleFont(); + $font->setName('Calibri'); + Font::getTrueTypeFontFileFromFont($font); + } +} From 290d0731fe7c9508b9226c0e502a4768eee646b1 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 30 Jul 2022 10:27:31 +0200 Subject: [PATCH 3/4] Allow multiple delimiters for `TEXTBEFORE()` and `TEXTAFTER()` functions --- .../Calculation/TextData/Extract.php | 48 ++++++++++++++----- .../Functions/TextData/TextAfterTest.php | 23 ++------- .../Functions/TextData/TextBeforeTest.php | 7 ++- tests/data/Calculation/TextData/TEXTAFTER.php | 36 ++++++++++++++ .../data/Calculation/TextData/TEXTBEFORE.php | 36 ++++++++++++++ 5 files changed, 119 insertions(+), 31 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php index 1a9e84db..ee7e31b7 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -104,7 +104,8 @@ class Extract * * @param mixed $text the text that you're searching * Or can be an array of values - * @param ?string $delimiter the text that marks the point before which you want to extract + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values * @param mixed $instance The instance of the delimiter after which you want to extract the text. * By default, this is the first instance (1). * A negative value means start searching from the end of the text string. @@ -132,7 +133,6 @@ class Extract } $text = Helpers::extractString($text ?? ''); - $delimiter = Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')); $instance = (int) $instance; $matchMode = (int) $matchMode; $matchEnd = (int) $matchEnd; @@ -141,13 +141,14 @@ class Extract if (is_array($split) === false) { return $split; } - if ($delimiter === '') { + if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') { return ($instance > 0) ? '' : $text; } // Adjustment for a match as the first element of the split $flags = self::matchFlags($matchMode); - $adjust = preg_match('/^' . preg_quote($delimiter) . "\$/{$flags}", $split[0]); + $delimiter = self::buildDelimiter($delimiter); + $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]); $oddReverseAdjustment = count($split) % 2; $split = ($instance < 0) @@ -161,7 +162,8 @@ class Extract * TEXTAFTER. * * @param mixed $text the text that you're searching - * @param ?string $delimiter the text that marks the point before which you want to extract + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values * @param mixed $instance The instance of the delimiter after which you want to extract the text. * By default, this is the first instance (1). * A negative value means start searching from the end of the text string. @@ -189,7 +191,6 @@ class Extract } $text = Helpers::extractString($text ?? ''); - $delimiter = Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')); $instance = (int) $instance; $matchMode = (int) $matchMode; $matchEnd = (int) $matchEnd; @@ -198,13 +199,14 @@ class Extract if (is_array($split) === false) { return $split; } - if ($delimiter === '') { + if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') { return ($instance < 0) ? '' : $text; } // Adjustment for a match as the first element of the split $flags = self::matchFlags($matchMode); - $adjust = preg_match('/^' . preg_quote($delimiter) . "\$/{$flags}", $split[0]); + $delimiter = self::buildDelimiter($delimiter); + $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]); $oddReverseAdjustment = count($split) % 2; $split = ($instance < 0) @@ -215,21 +217,23 @@ class Extract } /** + * @param null|array|string $delimiter * @param int $matchMode * @param int $matchEnd * @param mixed $ifNotFound * * @return string|string[] */ - private static function validateTextBeforeAfter(string $text, string $delimiter, int $instance, $matchMode, $matchEnd, $ifNotFound) + private static function validateTextBeforeAfter(string $text, $delimiter, int $instance, $matchMode, $matchEnd, $ifNotFound) { $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); - if (preg_match('/' . preg_quote($delimiter) . "/{$flags}", $text) === 0 && $matchEnd === 0) { + if (preg_match('/' . $delimiter . "/{$flags}", $text) === 0 && $matchEnd === 0) { return $ifNotFound; } - $split = preg_split('/(' . preg_quote($delimiter) . ")/{$flags}", $text, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $split = preg_split('/' . $delimiter . "/{$flags}", $text, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); if ($split === false) { return ExcelError::NA(); } @@ -247,6 +251,28 @@ class Extract return $split; } + /** + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + */ + private static function buildDelimiter($delimiter): string + { + if (is_array($delimiter)) { + $delimiter = Functions::flattenArray($delimiter); + $quotedDelimiters = array_map( + function ($delimiter) { + return preg_quote($delimiter ?? ''); + }, + $delimiter + ); + $delimiters = implode('|', $quotedDelimiters); + + return '(' . $delimiters . ')'; + } + + return '(' . preg_quote($delimiter ?? '') . ')'; + } + private static function matchFlags(int $matchMode): string { return ($matchMode === 0) ? 'mu' : 'miu'; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php index b3b01d24..00483260 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php @@ -2,8 +2,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData; -use PhpOffice\PhpSpreadsheet\Calculation\Calculation; - class TextAfterTest extends AllSetupTeardown { /** @@ -14,14 +12,17 @@ class TextAfterTest extends AllSetupTeardown $text = $arguments[0]; $delimiter = $arguments[1]; - $args = 'A1, A2'; + $args = (is_array($delimiter)) ? 'A1, {A2,A3}' : 'A1, A2'; $args .= (isset($arguments[2])) ? ", {$arguments[2]}" : ','; $args .= (isset($arguments[3])) ? ", {$arguments[3]}" : ','; $args .= (isset($arguments[4])) ? ", {$arguments[4]}" : ','; $worksheet = $this->getSheet(); $worksheet->getCell('A1')->setValue($text); - $worksheet->getCell('A2')->setValue($delimiter); + $worksheet->getCell('A2')->setValue((is_array($delimiter)) ? $delimiter[0] : $delimiter); + if (is_array($delimiter)) { + $worksheet->getCell('A3')->setValue($delimiter[1]); + } $worksheet->getCell('B1')->setValue("=TEXTAFTER({$args})"); $result = $worksheet->getCell('B1')->getCalculatedValue(); @@ -32,18 +33,4 @@ class TextAfterTest extends AllSetupTeardown { return require 'tests/data/Calculation/TextData/TEXTAFTER.php'; } - - public function testTextAfterWithArray(): void - { - $calculation = Calculation::getInstance(); - - $text = "Red Riding Hood's red riding hood"; - $delimiter = 'red'; - - $args = "\"{$text}\", \"{$delimiter}\", 1, {0;1}"; - - $formula = "=TEXTAFTER({$args})"; - $result = $calculation->_calculateFormulaValue($formula); - self::assertEquals([[' riding hood'], [" Riding Hood's red riding hood"]], $result); - } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php index 17938b5e..37e46636 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php @@ -12,14 +12,17 @@ class TextBeforeTest extends AllSetupTeardown $text = $arguments[0]; $delimiter = $arguments[1]; - $args = 'A1, A2'; + $args = (is_array($delimiter)) ? 'A1, {A2,A3}' : 'A1, A2'; $args .= (isset($arguments[2])) ? ", {$arguments[2]}" : ','; $args .= (isset($arguments[3])) ? ", {$arguments[3]}" : ','; $args .= (isset($arguments[4])) ? ", {$arguments[4]}" : ','; $worksheet = $this->getSheet(); $worksheet->getCell('A1')->setValue($text); - $worksheet->getCell('A2')->setValue($delimiter); + $worksheet->getCell('A2')->setValue((is_array($delimiter)) ? $delimiter[0] : $delimiter); + if (is_array($delimiter)) { + $worksheet->getCell('A3')->setValue($delimiter[1]); + } $worksheet->getCell('B1')->setValue("=TEXTBEFORE({$args})"); $result = $worksheet->getCell('B1')->getCalculatedValue(); diff --git a/tests/data/Calculation/TextData/TEXTAFTER.php b/tests/data/Calculation/TextData/TEXTAFTER.php index c594ecdc..ebcfecb9 100644 --- a/tests/data/Calculation/TextData/TEXTAFTER.php +++ b/tests/data/Calculation/TextData/TEXTAFTER.php @@ -212,4 +212,40 @@ return [ 1, ], ], + 'Multi-delimiter Case-Insensitive Offset 1' => [ + " riding hood's red riding hood", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 1, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 2' => [ + "'s red riding hood", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 2, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 3' => [ + ' riding hood', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 3, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset -2' => [ + ' riding hood', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + -2, + 1, + ], + ], ]; diff --git a/tests/data/Calculation/TextData/TEXTBEFORE.php b/tests/data/Calculation/TextData/TEXTBEFORE.php index f94d5f28..1929354c 100644 --- a/tests/data/Calculation/TextData/TEXTBEFORE.php +++ b/tests/data/Calculation/TextData/TEXTBEFORE.php @@ -204,4 +204,40 @@ return [ 1, ], ], + 'Multi-delimiter Case-Insensitive Offset 1' => [ + 'Little ', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 1, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 2' => [ + 'Little Red riding ', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 2, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 3' => [ + "Little Red riding hood's ", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 3, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset -2' => [ + "Little Red riding hood's ", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + -2, + 1, + ], + ], ]; From db2bc3b28913d33fb762f7ca543e5b2d84b7b398 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 06:03:17 -0700 Subject: [PATCH 4/4] Bump phpstan/phpstan from 1.8.0 to 1.8.2 (#2977) Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan) from 1.8.0 to 1.8.2. - [Release notes](https://github.com/phpstan/phpstan/releases) - [Changelog](https://github.com/phpstan/phpstan/blob/1.8.x/CHANGELOG.md) - [Commits](https://github.com/phpstan/phpstan/compare/1.8.0...1.8.2) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.lock b/composer.lock index 4ac9f631..3f82754d 100644 --- a/composer.lock +++ b/composer.lock @@ -763,12 +763,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "04f4e8f6716241cb9200774ff73cb99fbb81e09a" + "reference": "231b4e82eee01f16537c8ac3fce31c1f83320c80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/04f4e8f6716241cb9200774ff73cb99fbb81e09a", - "reference": "04f4e8f6716241cb9200774ff73cb99fbb81e09a", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/231b4e82eee01f16537c8ac3fce31c1f83320c80", + "reference": "231b4e82eee01f16537c8ac3fce31c1f83320c80", "shasum": "" }, "require": { @@ -829,7 +829,7 @@ "stylecheck", "tests" ], - "time": "2022-06-26T10:27:07+00:00" + "time": "2022-07-26T12:51:47+00:00" }, { "name": "doctrine/annotations", @@ -2076,16 +2076,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.8.0", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "b7648d4ee9321665acaf112e49da9fd93df8fbd5" + "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b7648d4ee9321665acaf112e49da9fd93df8fbd5", - "reference": "b7648d4ee9321665acaf112e49da9fd93df8fbd5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c53312ecc575caf07b0e90dee43883fdf90ca67c", + "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c", "shasum": "" }, "require": { @@ -2127,7 +2127,7 @@ "type": "tidelift" } ], - "time": "2022-06-29T08:53:31+00:00" + "time": "2022-07-20T09:57:31+00:00" }, { "name": "phpstan/phpstan-phpunit",