Skip to content

Commit

Permalink
Sheet Background Images
Browse files Browse the repository at this point in the history
Fix PHPOffice#1649, a 3-year-old issue long marked "stale". Excel supports background images on sheets; now PhpSpreadsheet will as well. Support is limited to Xlsx (read and write) and Html (write only). As far as I can tell, Excel Xml and Gnumeric do not support this, nor, of course, do Csv and Slk; Excel Xls does, but, as usual, how to handle it in BIFF format is a mystery; LibreOffice ODS supports it differently than Excel, and this is just another of many ODS style properties not currently supported by PhpSpreadsheet.
  • Loading branch information
oleibman committed Nov 19, 2023
1 parent f9eb35d commit 234f54e
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 9 deletions.
22 changes: 22 additions & 0 deletions src/PhpSpreadsheet/Reader/Xlsx.php
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet

if ($this->readDataOnly === false) {
$this->readAutoFilter($xmlSheetNS, $docSheet);
$this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels');
}

$this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS);
Expand Down Expand Up @@ -2201,6 +2202,27 @@ private function readAutoFilter(
}
}

private function readBackgroundImage(
SimpleXMLElement $xmlSheet,
Worksheet $docSheet,
string $relsName
): void {
if ($xmlSheet && $xmlSheet->picture) {
$id = (string) self::getArrayItem(self::getAttributes($xmlSheet->picture, Namespaces::SCHEMA_OFFICE_DOCUMENT), 'id');
$rels = $this->loadZip($relsName);
foreach ($rels->Relationship as $rel) {
$attrs = $rel->attributes() ?? [];
$rid = (string) ($attrs['Id'] ?? '');
$target = (string) ($attrs['Target'] ?? '');
if ($rid === $id && substr($target, 0, 2) === '..') {
$target = 'xl' . substr($target, 2);
$content = $this->getFromZipArchive($this->zip, $target);
$docSheet->setBackgroundImage($content);
}
}
}
}

private function readTables(
SimpleXMLElement $xmlSheet,
Worksheet $docSheet,
Expand Down
43 changes: 43 additions & 0 deletions src/PhpSpreadsheet/Worksheet/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -3879,4 +3879,47 @@ private function getXfIndex(string $coordinate): ?int

return $xfIndex;
}

private string $backgroundImage = '';

private string $backgroundMime = '';

private string $backgroundExtension = '';

public function getBackgroundImage(): string
{
return $this->backgroundImage;
}

public function getBackgroundMime(): string
{
return $this->backgroundMime;
}

public function getBackgroundExtension(): string
{
return $this->backgroundExtension;
}

/**
* Set background image.
* Used on read/write for Xlsx.
* Used on write for Html.
*
* @param string $backgroundImage Image represented as a string, e.g. results of file_get_contents
*/
public function setBackgroundImage(string $backgroundImage): self
{
$imageArray = getimagesizefromstring($backgroundImage) ?: ['mime' => ''];
$mime = $imageArray['mime'];
if ($mime !== '') {
$extension = explode('/', $mime);
$extension = $extension[1];
$this->backgroundImage = $backgroundImage;
$this->backgroundMime = $mime;
$this->backgroundExtension = $extension;
}

return $this;
}
}
5 changes: 5 additions & 0 deletions src/PhpSpreadsheet/Writer/Html.php
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,11 @@ private function buildCssPerSheet(Worksheet $sheet, array &$css): void
$css["table.sheet$sheetIndex"]['page-break-inside'] = 'avoid';
$css["table.sheet$sheetIndex"]['break-inside'] = 'avoid';
}
$picture = $sheet->getBackgroundImage();
if ($picture !== '') {
$base64 = base64_encode($picture);
$css["table.sheet$sheetIndex"]['background-image'] = 'url(data:' . $sheet->getBackgroundMime() . ';base64,' . $base64 . ')';
}

// Build styles
// Calculate column widths
Expand Down
2 changes: 1 addition & 1 deletion src/PhpSpreadsheet/Writer/Xlsx.php
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ public function save($filename, int $flags = 0): void
// Add worksheet relationships (drawings, ...)
for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
// Add relationships
$zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts, $tableRef1);
$zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts, $tableRef1, $zipContent);

// Add unparsedLoadedData
$sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName();
Expand Down
7 changes: 7 additions & 0 deletions src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ public function writeContentTypes(Spreadsheet $spreadsheet, $includeCharts = fal
}
}
}

$bgImage = $spreadsheet->getSheet($i)->getBackgroundImage();
$mimeType = $spreadsheet->getSheet($i)->getBackgroundMime();
$extension = $spreadsheet->getSheet($i)->getBackgroundExtension();
if ($bgImage !== '' && !isset($aMediaContentTypes[$mimeType])) {
$this->writeDefaultContentType($objWriter, $extension, $mimeType);
}
}

// unparsed defaults
Expand Down
16 changes: 15 additions & 1 deletion src/PhpSpreadsheet/Writer/Xlsx/Rels.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public function writeWorkbookRelationships(Spreadsheet $spreadsheet)
*
* @return string XML Output
*/
public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false, $tableRef = 1)
public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false, $tableRef = 1, array &$zipContent = [])
{
// Create XML writer
$objWriter = null;
Expand Down Expand Up @@ -221,6 +221,20 @@ public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\
);
}

$backgroundImage = $worksheet->getBackgroundImage();
if ($backgroundImage !== '') {
$rId = 'Bg';
$uniqueName = md5(mt_rand(0, 9999) . time() . mt_rand(0, 9999));
$relPath = "../media/$uniqueName." . $worksheet->getBackgroundExtension();
$this->writeRelationship(
$objWriter,
$rId,
Namespaces::IMAGE,
$relPath
);
$zipContent["xl/media/$uniqueName." . $worksheet->getBackgroundExtension()] = $backgroundImage;
}

// Write hyperlink relationships?
$i = 1;
foreach ($worksheet->getHyperlinkCollection() as $hyperlink) {
Expand Down
44 changes: 37 additions & 7 deletions src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, array $string
// IgnoredErrors
$this->writeIgnoredErrors($objWriter);

// BackgroundImage must come after ignored, before table
$this->writeBackgroundImage($objWriter, $worksheet);

// Table
$this->writeTable($objWriter, $worksheet);

Expand Down Expand Up @@ -1042,6 +1045,9 @@ private function writeAutoFilter(XMLWriter $objWriter, PhpspreadsheetWorksheet $
private function writeTable(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
{
$tableCount = $worksheet->getTableCollection()->count();
if ($tableCount === 0) {
return;
}

$objWriter->startElement('tableParts');
$objWriter->writeAttribute('count', (string) $tableCount);
Expand All @@ -1055,6 +1061,18 @@ private function writeTable(XMLWriter $objWriter, PhpspreadsheetWorksheet $works
$objWriter->endElement();
}

/**
* Write Background Image.
*/
private function writeBackgroundImage(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
{
if ($worksheet->getBackgroundImage() !== '') {
$objWriter->startElement('picture');
$objWriter->writeAttribute('r:id', 'rIdBg');
$objWriter->endElement();
}
}

/**
* Write PageSetup.
*/
Expand Down Expand Up @@ -1098,19 +1116,31 @@ private function writePageSetup(XMLWriter $objWriter, PhpspreadsheetWorksheet $w
private function writeHeaderFooter(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
{
// headerFooter
$headerFooter = $worksheet->getHeaderFooter();
$oddHeader = $headerFooter->getOddHeader();
$oddFooter = $headerFooter->getOddFooter();
$evenHeader = $headerFooter->getEvenHeader();
$evenFooter = $headerFooter->getEvenFooter();
$firstHeader = $headerFooter->getFirstHeader();
$firstFooter = $headerFooter->getFirstFooter();
if ("$oddHeader$oddFooter$evenHeader$evenFooter$firstHeader$firstFooter" === '') {
return;
}

$objWriter->startElement('headerFooter');
$objWriter->writeAttribute('differentOddEven', ($worksheet->getHeaderFooter()->getDifferentOddEven() ? 'true' : 'false'));
$objWriter->writeAttribute('differentFirst', ($worksheet->getHeaderFooter()->getDifferentFirst() ? 'true' : 'false'));
$objWriter->writeAttribute('scaleWithDoc', ($worksheet->getHeaderFooter()->getScaleWithDocument() ? 'true' : 'false'));
$objWriter->writeAttribute('alignWithMargins', ($worksheet->getHeaderFooter()->getAlignWithMargins() ? 'true' : 'false'));

$objWriter->writeElement('oddHeader', $worksheet->getHeaderFooter()->getOddHeader());
$objWriter->writeElement('oddFooter', $worksheet->getHeaderFooter()->getOddFooter());
$objWriter->writeElement('evenHeader', $worksheet->getHeaderFooter()->getEvenHeader());
$objWriter->writeElement('evenFooter', $worksheet->getHeaderFooter()->getEvenFooter());
$objWriter->writeElement('firstHeader', $worksheet->getHeaderFooter()->getFirstHeader());
$objWriter->writeElement('firstFooter', $worksheet->getHeaderFooter()->getFirstFooter());
$objWriter->endElement();
self::writeElementIf($objWriter, $oddHeader !== '', 'oddHeader', $oddHeader);
self::writeElementIf($objWriter, $oddFooter !== '', 'oddFooter', $oddFooter);
self::writeElementIf($objWriter, $evenHeader !== '', 'evenHeader', $evenHeader);
self::writeElementIf($objWriter, $evenFooter !== '', 'evenFooter', $evenFooter);
self::writeElementIf($objWriter, $firstHeader !== '', 'firstHeader', $firstHeader);
self::writeElementIf($objWriter, $firstFooter !== '', 'firstFooter', $firstFooter);

$objWriter->endElement(); // headerFooter
}

/**
Expand Down
31 changes: 31 additions & 0 deletions tests/PhpSpreadsheetTests/Writer/Html/BackgroundImageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Writer\Html;

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Html;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;

class BackgroundImageTest extends AbstractFunctional
{
public function testBackgroundImage(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getCell('A1')->setValue(1);
$sheet->getCell('B1')->setValue(2);
$sheet->getCell('A2')->setValue(3);
$sheet->getCell('B2')->setValue(4);
$imageFile = 'tests/data/Writer/XLSX/backgroundtest.png';
$image = (string) file_get_contents($imageFile);
$sheet->setBackgroundImage($image);
self::assertSame('image/png', $sheet->getBackgroundMime());
self::assertSame('png', $sheet->getBackgroundExtension());
$writer = new Html($spreadsheet);
$header = $writer->generateHTMLHeader(true);
self::assertStringContainsString('table.sheet0 { background-image:url(data:image/png;base64,', $header);
$spreadsheet->disconnectWorksheets();
}
}
50 changes: 50 additions & 0 deletions tests/PhpSpreadsheetTests/Writer/Xlsx/BackgroundImageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;

class BackgroundImageTest extends AbstractFunctional
{
public function testBackgroundImage(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getCell('A1')->setValue(1);
$sheet->getCell('B1')->setValue(2);
$sheet->getCell('A2')->setValue(3);
$sheet->getCell('B2')->setValue(4);
$imageFile = 'tests/data/Writer/XLSX/backgroundtest.png';
$image = (string) file_get_contents($imageFile);
$sheet->setBackgroundImage($image);
self::assertSame('image/png', $sheet->getBackgroundMime());
self::assertSame('png', $sheet->getBackgroundExtension());

$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
$spreadsheet->disconnectWorksheets();
$reloadedWorksheet = $reloadedSpreadsheet->getActiveSheet();
self::assertSame($image, $reloadedWorksheet->getBackgroundImage());
self::assertSame('image/png', $reloadedWorksheet->getBackgroundMime());
self::assertSame('png', $reloadedWorksheet->getBackgroundExtension());
self::assertSame(2, $reloadedWorksheet->getCell('B1')->getValue());
$reloadedSpreadsheet->disconnectWorksheets();
}

public function testInvalidImage(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getCell('A1')->setValue(1);
$imageFile = __FILE__;
$image = (string) file_get_contents($imageFile);
self::assertNotSame('', $image);
$sheet->setBackgroundImage($image);
self::assertSame('', $sheet->getBackgroundImage());
self::assertSame('', $sheet->getBackgroundMime());
self::assertSame('', $sheet->getBackgroundExtension());
$spreadsheet->disconnectWorksheets();
}
}
Binary file added tests/data/Writer/XLSX/backgroundtest.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 234f54e

Please sign in to comment.