Xlsx Writer Support Data URI for Images

Fix #4823. When writing a spreadsheet to Html, a data Uri can be used if `embedImages` is true. Reading such an Html spreadsheet and attempting to write it to Xlsx results in an Exception. It should be noted that Excel itself cannot open the Html properly; none of the images are present.

The PhpSpreadsheet problem arises not with the inclusion of the image, but rather with attempting to include the appropriate entry in `[ContentTypes].xml`. This PR corrects that problem.

For the record, Xls Writer does not have a problem with this situation. Just to demonstrate that, a parallel test for Xls Writer is added in addition to the new Xlsx Writer test.
This commit is contained in:
oleibman
2026-03-10 19:52:51 -07:00
parent a1dacfdf79
commit dfab88262a
5 changed files with 145 additions and 2 deletions
+6 -2
View File
@@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Worksheet;
use Composer\Pcre\Preg;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use ZipArchive;
@@ -58,6 +59,9 @@ class Drawing extends BaseDrawing
*/
public function getExtension(): string
{
if (Preg::isMatch('~^data:image/([^;]+);base64,~', $this->path, $matches)) {
return $matches[1];
}
$exploded = explode('.', basename($this->path));
return $exploded[count($exploded) - 1];
@@ -96,7 +100,7 @@ class Drawing extends BaseDrawing
public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip = null, bool $allowExternal = true, ?callable $isWhitelisted = null): static
{
$this->isUrl = false;
if (preg_match('~^data:image/[a-z]+;base64,~', $path) === 1) {
if (Preg::isMatch('~^data:image/[a-z]+;base64,~', $path)) {
$this->path = $path;
return $this;
@@ -114,7 +118,7 @@ class Drawing extends BaseDrawing
}
// Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979
} elseif (filter_var($path, FILTER_VALIDATE_URL) || (preg_match('/^([\w\s\x00-\x1f]+):/u', $path) && !preg_match('/^([\w]+):/u', $path))) {
if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) {
if (!Preg::isMatch('/^(http|https|file|ftp|s3):/', $path)) {
throw new PhpSpreadsheetException('Invalid protocol for linked drawing');
}
if (!$allowExternal) {
@@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Composer\Pcre\Preg;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
@@ -281,6 +282,9 @@ class ContentTypes extends WriterPart
*/
private function getImageMimeType(string $filename): string
{
if (Preg::isMatch('~^data:(image/[^;]+);base64,~', $filename, $matches)) {
return $matches[1];
}
if (File::fileExists($filename)) {
$image = getimagesize($filename);
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xls;
use PhpOffice\PhpSpreadsheet\Reader\Html as HtmlReader;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class Issue4823Test extends AbstractFunctional
{
/**
* Xlsx Writer did not handle image data uri's correctly (Xls was okay).
*/
public function testIssue4823(): void
{
$infile = 'tests/data/Reader/HTML/issue.4823.html';
$reader = new HtmlReader();
$spreadsheet = $reader->load($infile);
$sheet = $spreadsheet->getActiveSheet();
$drawings = $sheet->getDrawingCollection();
self::assertCount(1, $drawings);
$drawing = $drawings[0];
self::assertInstanceOf(Drawing::class, $drawing);
self::assertStringStartsWith('data:image/png;base64,', $drawing->getPath());
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls');
$spreadsheet->disconnectWorksheets();
$rsheet = $reloadedSpreadsheet->getActiveSheet();
$drawings = $rsheet->getDrawingCollection();
self::assertCount(1, $drawings);
$drawing = $drawings[0];
self::assertInstanceOf(MemoryDrawing::class, $drawing);
self::assertSame('image/png', $drawing->getMimeType());
$reloadedSpreadsheet->disconnectWorksheets();
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Reader\Html as HtmlReader;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class Issue4823Test extends AbstractFunctional
{
/**
* Xlsx Writer did not handle image data uri's correctly (Xls was okay).
*/
public function testIssue4823(): void
{
$infile = 'tests/data/Reader/HTML/issue.4823.html';
$reader = new HtmlReader();
$spreadsheet = $reader->load($infile);
$sheet = $spreadsheet->getActiveSheet();
$drawings = $sheet->getDrawingCollection();
self::assertCount(1, $drawings);
$drawing = $drawings[0];
self::assertInstanceOf(Drawing::class, $drawing);
self::assertStringStartsWith('data:image/png;base64,', $drawing->getPath());
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
$spreadsheet->disconnectWorksheets();
$rsheet = $reloadedSpreadsheet->getActiveSheet();
$drawings = $rsheet->getDrawingCollection();
self::assertCount(1, $drawings);
$drawing = $drawings[0];
self::assertNotNull($drawing);
self::assertInstanceOf(Drawing::class, $drawing);
self::assertSame(1, preg_match('~^zip:.*/\w+[.]png$~', $drawing->getPath()));
$reloadedSpreadsheet->disconnectWorksheets();
}
}
+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet" />
<title>Untitled Spreadsheet</title>
<meta name="author" content="Unknown Creator" />
<meta name="title" content="Untitled Spreadsheet" />
<meta name="lastModifiedBy" content="Unknown Creator" />
<meta name="created" content="2026-03-09T20:10:17-07:00" />
<meta name="modified" content="2026-03-09T20:10:17-07:00" />
<style type="text/css">
html { font-family:Calibri, Arial, Helvetica, sans-serif; font-size:11pt; background-color:white }
a.comment-indicator:hover + div.comment { background:#ffd; position:absolute; display:block; border:1px solid black; padding:0.5em }
a.comment-indicator { background:red; display:inline-block; border:1px solid black; width:0.5em; height:0.5em }
div.comment { display:none }
table { border-collapse:collapse }
.b { text-align:center }
.e { text-align:center }
.f { text-align:right }
.inlineStr { text-align:left }
.n { text-align:right }
.s { text-align:left }
.floatright { float:right }
.floatleft { float:left }
td.style0, th.style0 { vertical-align:bottom; border-bottom:none #000000; border-top:none #000000; border-left:none #000000; border-right:none #000000; color:#000000; font-family:'Calibri'; font-size:11pt }
table.sheet0 col.col0 { width:42pt }
table.sheet0 tr { height:15pt }
@page page0 { margin-left: 0.7in; margin-right: 0.7in; margin-top: 0.75in; margin-bottom: 0.75in; }
.navigation {page-break-after: always;}
.scrpgbrk, div + div {page-break-before: always;}
@media screen {
.gridlines td {border: 1px solid black;}
.gridlines th {border: 1px solid black;}
body>div {margin-top: 5px;}
body>div:first-child {margin-top: 0;}
.scrpgbrk {margin-top: 1px;}
}
@media print {
.gridlinesp td {border: 1px solid black;}
.gridlinesp th {border: 1px solid black;}
.navigation {display: none;}
}
</style>
</head>
<body>
<div style='page: page0'>
<table id='sheet0' class='sheet0 gridlines'>
<col class="col0" />
<tbody>
<tr class="row0">
<td class="column0 style0 null" style='position: relative;'>
<img style="position: absolute; z-index: 1; left: 0px; top: 0px; width: 100px; height: 100px;" src="data:image/png;base64,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" alt="Embedded image" />&nbsp;</td>
</tr>
</tbody></table>
</div>
</body>
</html>