diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index e9fbabb415..4699671e43 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -15,6 +15,7 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Protection; use PhpOffice\PhpSpreadsheet\Style\Style; +use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Table; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Stringable; @@ -307,6 +308,12 @@ public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE $this->value = SharedDate::convertIsoDate($value); $dataType = DataType::TYPE_NUMERIC; + break; + case DataType::TYPE_DRAWING_IN_CELL: + if ($value instanceof BaseDrawing) { + $this->value = $value; + } + break; case DataType::TYPE_ERROR: $this->value = DataType::checkErrorCode($value); diff --git a/src/PhpSpreadsheet/Cell/DataType.php b/src/PhpSpreadsheet/Cell/DataType.php index 774a57b23f..71cde4aca2 100644 --- a/src/PhpSpreadsheet/Cell/DataType.php +++ b/src/PhpSpreadsheet/Cell/DataType.php @@ -17,6 +17,7 @@ class DataType const TYPE_INLINE = 'inlineStr'; const TYPE_ERROR = 'e'; const TYPE_ISO_DATE = 'd'; + const TYPE_DRAWING_IN_CELL = 'drawingCell'; /** * List of error codes. diff --git a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php index 42a45a913b..daae358102 100644 --- a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php +++ b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; +use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use Stringable; class DefaultValueBinder implements IValueBinder @@ -33,6 +34,11 @@ public function bindValue(Cell $cell, mixed $value): bool $value = $value->format('Y-m-d H:i:s'); } elseif ($value instanceof Stringable) { $value = (string) $value; + } elseif ($value instanceof BaseDrawing) { + $value->setCoordinates($cell->getCoordinate()); + $value->setResizeProportional(false); + $value->setInCell(true); + $value->setWorksheet($cell->getWorksheet()); } else { throw new SpreadsheetException('Unable to bind unstringable ' . gettype($value)); } @@ -68,6 +74,9 @@ public static function dataTypeForValue(mixed $value): string if ($value instanceof RichText) { return DataType::TYPE_INLINE; } + if ($value instanceof BaseDrawing) { + return DataType::TYPE_DRAWING_IN_CELL; + } if ($value instanceof Stringable) { $value = (string) $value; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index f8a0b11324..6a7795f653 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -784,6 +784,19 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $charts = $chartDetails = []; + // Add richData (contains relation of in-cell images) + $richData = []; + $relationsFileName = $dir . '/richData/_rels/richValueRel.xml.rels'; + if ($zip->locateName($relationsFileName)) { + $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS); + foreach ($relsWorksheet->Relationship as $elex) { + $ele = self::getAttributes($elex); + if ($ele['Type'] == Namespaces::IMAGE) { + $richData['image'][(string) $ele['Id']] = (string) $ele['Target']; + } + } + } + $sheetCreated = false; if ($xmlWorkbookNS->sheets) { foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) { @@ -940,6 +953,27 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet break; case DataType::TYPE_ERROR: + if (isset($cAttr->vm, $richData['image']['rId' . $cAttr->vm]) && !$useFormula) { + $imagePath = $dir . '/' . str_replace('../', '', $richData['image']['rId' . $cAttr->vm]); + $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); + $objDrawing->setPath( + 'zip://' . File::realpath($filename) . '#' . $imagePath, + false, + $zip + ); + + $objDrawing->setCoordinates($r); + $objDrawing->setResizeProportional(false); + $objDrawing->setInCell(true); + $objDrawing->setWorksheet($docSheet); + + $value = $objDrawing; + $cellDataType = DataType::TYPE_DRAWING_IN_CELL; + $c->t = DataType::TYPE_ERROR; + + break; + } + if (!$useFormula) { $value = self::castToError($c); } else { diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php index 7356cd8829..a6181eb225 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php @@ -121,4 +121,12 @@ class Namespaces const DYNAMIC_ARRAY = 'http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray'; const DYNAMIC_ARRAY_RICHDATA = 'http://schemas.microsoft.com/office/spreadsheetml/2017/richdata'; + + const RELATIONSHIPS_RICH_VALUE_TYPES = 'http://schemas.microsoft.com/office/2017/06/relationships/rdRichValueTypes'; + + const RELATIONSHIPS_RICH_VALUE_STRUCTURE = 'http://schemas.microsoft.com/office/2017/06/relationships/rdRichValueStructure'; + + const RELATIONSHIPS_RICH_VALUE = 'http://schemas.microsoft.com/office/2017/06/relationships/rdRichValue'; + + const RELATIONSHIPS_RICH_VALUE_REL = 'http://schemas.microsoft.com/office/2022/10/relationships/richValueRel'; } diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 5681e4aaa0..334156faa7 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -412,6 +412,21 @@ public function hasRibbonBinObjects(): bool return $this->ribbonBinObjects !== null; } + /** + * This workbook has in cell images. + */ + public function hasInCellDrawings(): bool + { + $sheetCount = $this->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + if ($this->getSheet($i)->getInCellDrawingCollection()->count() > 0) { + return true; + } + } + + return false; + } + /** * Check if a sheet with a specified code name already exists. * diff --git a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index faa836c545..2fd81366fe 100644 --- a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -137,6 +137,10 @@ class BaseDrawing implements IComparable */ protected ?int $opacity = null; + protected bool $inCell = false; + + protected int $index = 0; + /** * Create a new BaseDrawing. */ @@ -203,8 +207,13 @@ public function setWorksheet(?Worksheet $worksheet = null, bool $overrideOld = f if (!($this instanceof Drawing && $this->getPath() === '')) { $this->worksheet->getCell($this->coordinates); } - $this->worksheet->getDrawingCollection() - ->append($this); + if ($this->inCell) { + $this->worksheet->getInCellDrawingCollection() + ->append($this); + } else { + $this->worksheet->getDrawingCollection() + ->append($this); + } } } else { if ($overrideOld) { @@ -572,4 +581,28 @@ public function getOpacity(): ?int { return $this->opacity; } + + public function setInCell(bool $inCell): self + { + $this->inCell = $inCell; + + return $this; + } + + public function isInCell(): ?bool + { + return $this->inCell; + } + + public function setIndex(int $index): self + { + $this->index = $index; + + return $this; + } + + public function getIndex(): int + { + return $this->index; + } } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 64c4ec23a1..7b37e0d07e 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -108,6 +108,13 @@ class Worksheet */ private ArrayObject $drawingCollection; + /** + * Collection of drawings. + * + * @var ArrayObject + */ + private ArrayObject $inCellDrawingCollection; + /** * Collection of Chart objects. * @@ -334,6 +341,8 @@ public function __construct(?Spreadsheet $parent = null, string $title = 'Worksh $this->sheetView = new SheetView(); // Drawing collection $this->drawingCollection = new ArrayObject(); + // In Cell Drawing collection + $this->inCellDrawingCollection = new ArrayObject(); // Chart collection $this->chartCollection = new ArrayObject(); // Protection @@ -371,7 +380,7 @@ public function __destruct() ?->clearCalculationCacheForWorksheet($this->title); $this->disconnectCells(); - unset($this->rowDimensions, $this->columnDimensions, $this->tableCollection, $this->drawingCollection, $this->chartCollection, $this->autoFilter); + unset($this->rowDimensions, $this->columnDimensions, $this->tableCollection, $this->drawingCollection, $this->inCellDrawingCollection, $this->chartCollection, $this->autoFilter); } /** @@ -519,6 +528,16 @@ public function getDrawingCollection(): ArrayObject return $this->drawingCollection; } + /** + * Get collection of drawings. + * + * @return ArrayObject + */ + public function getInCellDrawingCollection(): ArrayObject + { + return $this->inCellDrawingCollection; + } + /** * Get collection of charts. * @@ -3730,6 +3749,13 @@ public function __clone() $newDrawing = clone $item; $newDrawing->setWorksheet($this); } + } elseif ($key === 'inCellDrawingCollection') { + $currentCollection = $this->inCellDrawingCollection; + $this->inCellDrawingCollection = new ArrayObject(); + foreach ($currentCollection as $item) { + $newDrawing = clone $item; + $newDrawing->setWorksheet($this); + } } elseif ($key === 'tableCollection') { $currentCollection = $this->tableCollection; $this->tableCollection = new ArrayObject(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index a38c930e42..53cad20b4a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -24,6 +24,7 @@ use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Rels; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RelsRibbon; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RelsVBA; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RichDataDrawing; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\StringTable; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Style; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Table; @@ -329,9 +330,22 @@ public function save($filename, int $flags = 0): void /** @var string[] */ $zipContent = []; + $richDataCount = 0; + + if ($this->spreadSheet->hasInCellDrawings()) { + $richDataDrawing = new RichDataDrawing(); + $richDataFiles = $richDataDrawing->generateFiles($this->spreadSheet); + $richDataCount = count($richDataFiles); + + // Add all Rich Data files to ZIP + foreach ($richDataFiles as $path => $content) { + $zipContent[$path] = $content; + } + } + // Add [Content_Types].xml to ZIP file $zipContent['[Content_Types].xml'] = $this->getWriterPartContentTypes()->writeContentTypes($this->spreadSheet, $this->includeCharts); - $metadataData = (new Xlsx\Metadata($this))->writeMetadata(); + $metadataData = (new Xlsx\Metadata($this))->writeMetadata($richDataCount); if ($metadataData !== '') { $zipContent['xl/metadata.xml'] = $metadataData; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php index 16c7bb978a..cdc10f7ffb 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php @@ -156,6 +156,14 @@ public function writeContentTypes(Spreadsheet $spreadsheet, bool $includeCharts $this->writeDefaultContentType($objWriter, $extension, $mimeType); } } + + if ($spreadsheet->hasInCellDrawings()) { + $this->writeOverrideContentType($objWriter, '/xl/richData/richValueRel.xml', 'application/vnd.ms-excel.richvaluerel+xml'); + $this->writeOverrideContentType($objWriter, '/xl/richData/rdrichvalue.xml', 'application/vnd.ms-excel.rdrichvalue+xml'); + $this->writeOverrideContentType($objWriter, '/xl/richData/rdrichvaluestructure.xml', 'application/vnd.ms-excel.rdrichvaluestructure+xml'); + $this->writeOverrideContentType($objWriter, '/xl/richData/rdRichValueTypes.xml', 'application/vnd.ms-excel.rdrichvaluetypes+xml'); + } + if ($spreadsheet->hasRibbonBinObjects()) { // Some additional objects in the ribbon ? // we need to write "Extension" but not already write for media content diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 887b436ea9..1abaa65edd 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -562,6 +562,12 @@ public function allDrawings(Spreadsheet $spreadsheet): array while ($iterator->valid()) { $aDrawings[] = $iterator->current(); + $iterator->next(); + } + $iterator = $spreadsheet->getSheet($i)->getInCellDrawingCollection()->getIterator(); + while ($iterator->valid()) { + $aDrawings[] = $iterator->current(); + $iterator->next(); } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Metadata.php b/src/PhpSpreadsheet/Writer/Xlsx/Metadata.php index 00c15f0003..c4cf925b93 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Metadata.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Metadata.php @@ -12,7 +12,7 @@ class Metadata extends WriterPart * * @return string XML Output */ - public function writeMetadata(): string + public function writeMetadata(int $richDataCount = 0): string { if (!$this->getParentWriter()->useDynamicArrays()) { return ''; @@ -32,29 +32,40 @@ public function writeMetadata(): string $objWriter->startElement('metadata'); $objWriter->writeAttribute('xmlns', Namespaces::MAIN); $objWriter->writeAttribute('xmlns:xlrd', Namespaces::DYNAMIC_ARRAY_RICHDATA); - $objWriter->writeAttribute('xmlns:xda', Namespaces::DYNAMIC_ARRAY); - $objWriter->startElement('metadataTypes'); - $objWriter->writeAttribute('count', '2'); + if ($richDataCount > 0) { + $objWriter->startElement('metadataTypes'); + $objWriter->writeAttribute('count', '1'); + $this->writeMetadataType($objWriter, 'XLRICHVALUE', false); + $objWriter->endElement(); // metadataTypes - $objWriter->startElement('metadataType'); - $objWriter->writeAttribute('name', 'XLDAPR'); - $objWriter->writeAttribute('minSupportedVersion', '120000'); - $objWriter->writeAttribute('copy', '1'); - $objWriter->writeAttribute('pasteAll', '1'); - $objWriter->writeAttribute('pasteValues', '1'); - $objWriter->writeAttribute('merge', '1'); - $objWriter->writeAttribute('splitFirst', '1'); - $objWriter->writeAttribute('rowColShift', '1'); - $objWriter->writeAttribute('clearFormats', '1'); - $objWriter->writeAttribute('clearComments', '1'); - $objWriter->writeAttribute('assign', '1'); - $objWriter->writeAttribute('coerce', '1'); - $objWriter->writeAttribute('cellMeta', '1'); - $objWriter->endElement(); // metadataType XLDAPR + $this->writeFutureMetadataXLRICHVALUE($objWriter, $richDataCount); + $this->writeValueMetadata($objWriter, $richDataCount); + } else { + $objWriter->writeAttribute('xmlns:xda', Namespaces::DYNAMIC_ARRAY); + + $objWriter->startElement('metadataTypes'); + $objWriter->writeAttribute('count', '2'); + $this->writeMetadataType($objWriter, 'XLDAPR'); + $this->writeMetadataType($objWriter, 'XLRICHVALUE'); + $objWriter->endElement(); // metadataTypes + + $this->writeFutureMetadataXLDAPR($objWriter, 1); + $this->writeFutureMetadataXLRICHVALUE($objWriter, 1); + $this->writeCellMetadata($objWriter, 1); + $this->writeValueMetadata($objWriter, 1, 2); + } + + $objWriter->endElement(); // metadata + + // Return + return $objWriter->getData(); + } + private function writeMetadataType(XMLWriter $objWriter, string $name, bool $cellMeta = true): void + { $objWriter->startElement('metadataType'); - $objWriter->writeAttribute('name', 'XLRICHVALUE'); + $objWriter->writeAttribute('name', $name); $objWriter->writeAttribute('minSupportedVersion', '120000'); $objWriter->writeAttribute('copy', '1'); $objWriter->writeAttribute('pasteAll', '1'); @@ -66,64 +77,85 @@ public function writeMetadata(): string $objWriter->writeAttribute('clearComments', '1'); $objWriter->writeAttribute('assign', '1'); $objWriter->writeAttribute('coerce', '1'); - $objWriter->endElement(); // metadataType XLRICHVALUE - $objWriter->endElement(); // metadataTypes + if ($cellMeta) { + $objWriter->writeAttribute('cellMeta', '1'); + } + $objWriter->endElement(); + } + private function writeFutureMetadataXLDAPR(XMLWriter $objWriter, int $count = 1): void + { $objWriter->startElement('futureMetadata'); $objWriter->writeAttribute('name', 'XLDAPR'); - $objWriter->writeAttribute('count', '1'); - $objWriter->startElement('bk'); - $objWriter->startElement('extLst'); - $objWriter->startElement('ext'); - $objWriter->writeAttribute('uri', '{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}'); - $objWriter->startElement('xda:dynamicArrayProperties'); - $objWriter->writeAttribute('fDynamic', '1'); - $objWriter->writeAttribute('fCollapsed', '0'); - $objWriter->endElement(); // xda:dynamicArrayProperties - $objWriter->endElement(); // ext - $objWriter->endElement(); // extLst - $objWriter->endElement(); // bk + $objWriter->writeAttribute('count', (string) $count); + + for ($index = 0; $index < $count; ++$index) { + $objWriter->startElement('bk'); + $objWriter->startElement('extLst'); + $objWriter->startElement('ext'); + $objWriter->writeAttribute('uri', '{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}'); + $objWriter->startElement('xda:dynamicArrayProperties'); + $objWriter->writeAttribute('fDynamic', '1'); + $objWriter->writeAttribute('fCollapsed', '0'); + $objWriter->endElement(); // xda:dynamicArrayProperties + $objWriter->endElement(); // ext + $objWriter->endElement(); // extLst + $objWriter->endElement(); // bk + } $objWriter->endElement(); // futureMetadata XLDAPR + } + private function writeFutureMetadataXLRICHVALUE(XMLWriter $objWriter, int $count): void + { $objWriter->startElement('futureMetadata'); $objWriter->writeAttribute('name', 'XLRICHVALUE'); - $objWriter->writeAttribute('count', '1'); - $objWriter->startElement('bk'); - $objWriter->startElement('extLst'); - $objWriter->startElement('ext'); - $objWriter->writeAttribute('uri', '{3e2802c4-a4d2-4d8b-9148-e3be6c30e623}'); - $objWriter->startElement('xlrd:rvb'); - $objWriter->writeAttribute('i', '0'); - $objWriter->endElement(); // xlrd:rvb - $objWriter->endElement(); // ext - $objWriter->endElement(); // extLst - $objWriter->endElement(); // bk + $objWriter->writeAttribute('count', (string) $count); + + for ($index = 0; $index < $count; ++$index) { + $objWriter->startElement('bk'); + $objWriter->startElement('extLst'); + $objWriter->startElement('ext'); + $objWriter->writeAttribute('uri', '{3e2802c4-a4d2-4d8b-9148-e3be6c30e623}'); + $objWriter->startElement('xlrd:rvb'); + $objWriter->writeAttribute('i', (string) $index); + $objWriter->endElement(); // xlrd:rvb + $objWriter->endElement(); // ext + $objWriter->endElement(); // extLst + $objWriter->endElement(); // bk + } $objWriter->endElement(); // futureMetadata XLRICHVALUE + } + private function writeCellMetadata(XMLWriter $objWriter, int $count = 1, int $t = 1): void + { $objWriter->startElement('cellMetadata'); - $objWriter->writeAttribute('count', '1'); - $objWriter->startElement('bk'); - $objWriter->startElement('rc'); - $objWriter->writeAttribute('t', '1'); - $objWriter->writeAttribute('v', '0'); - $objWriter->endElement(); // rc - $objWriter->endElement(); // bk + $objWriter->writeAttribute('count', (string) $count); + + for ($index = 0; $index < $count; ++$index) { + $objWriter->startElement('bk'); + $objWriter->startElement('rc'); + $objWriter->writeAttribute('t', (string) $t); + $objWriter->writeAttribute('v', (string) $index); + $objWriter->endElement(); // rc + $objWriter->endElement(); // bk + } $objWriter->endElement(); // cellMetadata + } + private function writeValueMetadata(XMLWriter $objWriter, int $count = 1, int $t = 1): void + { $objWriter->startElement('valueMetadata'); - $objWriter->writeAttribute('count', '1'); - $objWriter->startElement('bk'); - $objWriter->startElement('rc'); - $objWriter->writeAttribute('t', '2'); - $objWriter->writeAttribute('v', '0'); - $objWriter->endElement(); // rc - $objWriter->endElement(); // bk - $objWriter->endElement(); // valueMetadata + $objWriter->writeAttribute('count', (string) $count); - $objWriter->endElement(); // metadata - - // Return - return $objWriter->getData(); + for ($index = 0; $index < $count; ++$index) { + $objWriter->startElement('bk'); + $objWriter->startElement('rc'); + $objWriter->writeAttribute('t', (string) $t); + $objWriter->writeAttribute('v', (string) $index); + $objWriter->endElement(); // rc + $objWriter->endElement(); // bk + } + $objWriter->endElement(); // valueMetadata } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php index a913767dc3..3f798302b7 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -162,6 +162,14 @@ public function writeWorkbookRelationships(Spreadsheet $spreadsheet): string ++$i; //increment i if needed for an another relation } + if ($spreadsheet->getActiveSheet()->getInCellDrawingCollection()->count() > 0) { + $i = ($i + 1 + 3); + $this->writeRelationship($objWriter, $i, Namespaces::RELATIONSHIPS_RICH_VALUE, 'richData/rdrichvalue.xml'); + $this->writeRelationship($objWriter, ++$i, Namespaces::RELATIONSHIPS_RICH_VALUE_STRUCTURE, 'richData/rdrichvaluestructure.xml'); + $this->writeRelationship($objWriter, ++$i, Namespaces::RELATIONSHIPS_RICH_VALUE_TYPES, 'richData/rdRichValueTypes.xml'); + $this->writeRelationship($objWriter, ++$i, Namespaces::RELATIONSHIPS_RICH_VALUE_REL, 'richData/richValueRel.xml'); + } + $objWriter->endElement(); return $objWriter->getData(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/RichDataDrawing.php b/src/PhpSpreadsheet/Writer/Xlsx/RichDataDrawing.php new file mode 100644 index 0000000000..44b47b40df --- /dev/null +++ b/src/PhpSpreadsheet/Writer/Xlsx/RichDataDrawing.php @@ -0,0 +1,187 @@ + [path => XML content] + */ + public function generateFiles(Spreadsheet $spreadsheet): array + { + $worksheetCount = $spreadsheet->getSheetCount(); + + $index = 0; + for ($i = 0; $i < $worksheetCount; ++$i) { + $worksheet = $spreadsheet->getSheet($i); + $iterator = $worksheet->getInCellDrawingCollection()->getIterator(); + while ($iterator->valid()) { + /** @var Drawing $pDrawing */ + $pDrawing = $iterator->current(); + $indexedFilename = $pDrawing->getIndexedFilename(); + if (!isset($this->drawings[$indexedFilename])) { + $pDrawing->setIndex(++$index); + $this->drawings[$indexedFilename] = $pDrawing; + } else { + $pDrawing->setIndex($this->drawings[$indexedFilename]->getIndex()); + } + $iterator->next(); + } + } + + if (count($this->drawings) === 0) { + return []; + } + + return [ + 'xl/richData/rdrichvalue.xml' => $this->writeRdrichvalueXML(), + 'xl/richData/rdrichvaluestructure.xml' => $this->writeRdrichvaluestructureXML(), + 'xl/richData/rdRichValueTypes.xml' => $this->writeRdRichValueTypesXML(), + 'xl/richData/richValueRel.xml' => $this->writeRichValueRelXML(), + 'xl/richData/_rels/richValueRel.xml.rels' => $this->writeRichValueRelRelsXML(), + ]; + } + + private function writeRdrichvalueXML(): string + { + $xml = new XMLWriter(XMLWriter::STORAGE_MEMORY); + $xml->startDocument('1.0', 'UTF-8', 'yes'); + $xml->startElement('rvData'); + $xml->writeAttribute('xmlns', 'http://schemas.microsoft.com/office/spreadsheetml/2017/richdata'); + $xml->writeAttribute('count', (string) count($this->drawings)); + + $index = 0; + foreach ($this->drawings as $drawing) { + $xml->startElement('rv'); + $xml->writeAttribute('s', '0'); + $xml->writeElement('v', (string) $index++); + $xml->writeElement('v', '5'); + $xml->endElement(); // rv + } + + $xml->endElement(); // rvData + + return $xml->getData(); + } + + private function writeRdrichvaluestructureXML(): string + { + $xml = new XMLWriter(XMLWriter::STORAGE_MEMORY); + $xml->startDocument('1.0', 'UTF-8', 'yes'); + $xml->startElement('rvStructures'); + $xml->writeAttribute('xmlns', 'http://schemas.microsoft.com/office/spreadsheetml/2017/richdata'); + $xml->writeAttribute('count', '1'); + + $xml->startElement('s'); + $xml->writeAttribute('t', '_localImage'); + + $xml->startElement('k'); + $xml->writeAttribute('n', '_rvRel:LocalImageIdentifier'); + $xml->writeAttribute('t', 'i'); + $xml->endElement(); + + $xml->startElement('k'); + $xml->writeAttribute('n', 'CalcOrigin'); + $xml->writeAttribute('t', 'i'); + $xml->endElement(); + + $xml->endElement(); // s + + $xml->endElement(); // rvStructures + + return $xml->getData(); + } + + private function writeRdRichValueTypesXML(): string + { + $xml = new XMLWriter(XMLWriter::STORAGE_MEMORY); + $xml->startDocument('1.0', 'UTF-8', 'yes'); + $xml->startElement('rvTypesInfo'); + $xml->writeAttribute('xmlns', 'http://schemas.microsoft.com/office/spreadsheetml/2017/richdata2'); + $xml->writeAttribute('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006'); + $xml->writeAttribute('mc:Ignorable', 'x'); + $xml->writeAttribute('xmlns:x', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); + + $xml->startElement('global'); + $xml->startElement('keyFlags'); + + $keys = [ + '_Self', '_DisplayString', '_Flags', '_Format', + '_SubLabel', '_Attribution', '_Icon', '_Display', + '_CanonicalPropertyNames', '_ClassificationId', + ]; + + foreach ($keys as $key) { + $xml->startElement('key'); + $xml->writeAttribute('name', $key); + + $xml->startElement('flag'); + $xml->writeAttribute('name', 'ExcludeFromCalcComparison'); + $xml->writeAttribute('value', '1'); + if ($key === '_Self') { + $xml->startElement('flag'); + $xml->writeAttribute('name', 'ExcludeFromFile'); + $xml->writeAttribute('value', '1'); + $xml->endElement(); + } + $xml->endElement(); // flag + $xml->endElement(); // key + } + + $xml->endElement(); // keyFlags + $xml->endElement(); // global + $xml->endElement(); // rvTypesInfo + + return $xml->getData(); + } + + private function writeRichValueRelXML(): string + { + $xml = new XMLWriter(XMLWriter::STORAGE_MEMORY); + $xml->startDocument('1.0', 'UTF-8', 'yes'); + $xml->startElement('richValueRels'); + $xml->writeAttribute('xmlns', 'http://schemas.microsoft.com/office/spreadsheetml/2022/richvaluerel'); + $xml->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + + $index = 0; + foreach ($this->drawings as $drawing) { + $xml->startElement('rel'); + $xml->writeAttribute('r:id', 'rId' . ++$index); + $xml->endElement(); + } + + $xml->endElement(); // richValueRels + + return $xml->getData(); + } + + private function writeRichValueRelRelsXML(): string + { + $xml = new XMLWriter(XMLWriter::STORAGE_MEMORY); + $xml->startDocument('1.0', 'UTF-8', 'yes'); + $xml->startElement('Relationships'); + $xml->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships'); + + $index = 0; + foreach ($this->drawings as $drawing) { + $xml->startElement('Relationship'); + $xml->writeAttribute('Id', 'rId' . ++$index); + $xml->writeAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image'); + $xml->writeAttribute('Target', '../media/' . $drawing->getIndexedFilename()); + $xml->endElement(); + } + + $xml->endElement(); // Relationships + + return $xml->getData(); + } +} diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 5e445f6819..839aacdebb 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -18,6 +18,7 @@ use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalIconSet; use PhpOffice\PhpSpreadsheet\Style\Font; +use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension; use PhpOffice\PhpSpreadsheet\Worksheet\SheetView; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as PhpspreadsheetWorksheet; @@ -1557,6 +1558,13 @@ private function writeCellError(XMLWriter $objWriter, string $mappedType, string $objWriter->writeElement('v', $cellIsFormula ? $formulaerr : $cellValue); } + private function writeCellDrawing(XMLWriter $objWriter, int $index): void + { + $objWriter->writeAttribute('t', 'e'); + $objWriter->writeAttribute('vm', (string) $index); + $objWriter->writeElement('v', '#VALUE!'); + } + private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell $cell): void { $attributes = $cell->getFormulaAttributes() ?? []; @@ -1737,6 +1745,13 @@ private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksh case 'b': // Boolean $this->writeCellBoolean($objWriter, $mappedType, (bool) $cellValue); + break; + case 'drawingcell': // DrawingInCell + if ($cellValue instanceof BaseDrawing) { + $index = $cellValue->getIndex(); + $this->writeCellDrawing($objWriter, $index); + } + break; case 'e': // Error $this->writeCellError($objWriter, $mappedType, $cellValueString); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/DrawingInCellTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/DrawingInCellTest.php new file mode 100644 index 0000000000..345e292610 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/DrawingInCellTest.php @@ -0,0 +1,56 @@ +load($file); + + $sheet = $spreadsheet->getSheet(0); + $drawings = $sheet->getInCellDrawingCollection(); + self::assertCount(2, $drawings); + + if ($drawings[0] === null) { + self::fail('Unexpected null drawing'); + } else { + self::assertSame(IMAGETYPE_PNG, $drawings[0]->getType()); + self::assertSame('B2', $drawings[0]->getCoordinates()); + self::assertSame(0, $drawings[0]->getOffsetX()); + self::assertSame(0, $drawings[0]->getOffsetY()); + self::assertSame(296, $drawings[0]->getWidth()); + self::assertSame(154, $drawings[0]->getHeight()); + self::assertSame(296, $drawings[0]->getImageWidth()); + self::assertSame(154, $drawings[0]->getImageHeight()); + } + + self::assertSame($drawings[0], $sheet->getCell('B2')->getValue()); + + $sheet = $spreadsheet->getSheet(1); + $drawings = $sheet->getInCellDrawingCollection(); + self::assertCount(1, $drawings); + + if ($drawings[0] === null) { + self::fail('Unexpected null drawing'); + } else { + self::assertSame(IMAGETYPE_PNG, $drawings[0]->getType()); + self::assertSame('D7', $drawings[0]->getCoordinates()); + self::assertSame(0, $drawings[0]->getOffsetX()); + self::assertSame(0, $drawings[0]->getOffsetY()); + self::assertSame(413, $drawings[0]->getWidth()); + self::assertSame(218, $drawings[0]->getHeight()); + self::assertSame(413, $drawings[0]->getImageWidth()); + self::assertSame(218, $drawings[0]->getImageHeight()); + } + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingInCellTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingInCellTest.php new file mode 100644 index 0000000000..eb0bff202d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingInCellTest.php @@ -0,0 +1,97 @@ +load($file); + + // Save spreadsheet to file and read it back + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getSheet(0); + $drawings = $sheet->getInCellDrawingCollection(); + self::assertCount(2, $drawings); + + if ($drawings[0] === null) { + self::fail('Unexpected null drawing'); + } else { + self::assertSame(IMAGETYPE_PNG, $drawings[0]->getType()); + self::assertSame('B2', $drawings[0]->getCoordinates()); + self::assertSame(0, $drawings[0]->getOffsetX()); + self::assertSame(0, $drawings[0]->getOffsetY()); + self::assertSame(296, $drawings[0]->getWidth()); + self::assertSame(154, $drawings[0]->getHeight()); + self::assertSame(296, $drawings[0]->getImageWidth()); + self::assertSame(154, $drawings[0]->getImageHeight()); + } + + self::assertSame($drawings[0], $sheet->getCell('B2')->getValue()); + + $sheet = $reloadedSpreadsheet->getSheet(1); + $drawings = $sheet->getInCellDrawingCollection(); + self::assertCount(1, $drawings); + + if ($drawings[0] === null) { + self::fail('Unexpected null drawing'); + } else { + self::assertSame(IMAGETYPE_PNG, $drawings[0]->getType()); + self::assertSame('D7', $drawings[0]->getCoordinates()); + self::assertSame(0, $drawings[0]->getOffsetX()); + self::assertSame(0, $drawings[0]->getOffsetY()); + self::assertSame(413, $drawings[0]->getWidth()); + self::assertSame(218, $drawings[0]->getHeight()); + self::assertSame(413, $drawings[0]->getImageWidth()); + self::assertSame(218, $drawings[0]->getImageHeight()); + } + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testWriteNewPictureInCell(): void + { + $file = 'tests/data/Writer/XLSX/drawing_in_cell.xlsx'; + $reader = new Xlsx(); + $spreadsheet = $reader->load($file); + + $objDrawing = new Drawing(); + $objDrawing->setPath('tests/data/Writer/XLSX/blue_square.png'); + + $sheet = $spreadsheet->getSheet(1); + $sheet->getCell('C10')->setValue($objDrawing); + + // Save spreadsheet to file and read it back + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getSheet(1); + $drawings = $sheet->getInCellDrawingCollection(); + self::assertCount(2, $drawings); + + /** @var ?Drawing $drawing */ + $drawing = $sheet->getCell('C10')->getValue(); + + if ($drawing === null) { + self::fail('Unexpected null drawing'); + } else { + self::assertSame(IMAGETYPE_PNG, $drawing->getType()); + self::assertSame('C10', $drawing->getCoordinates()); + self::assertSame(100, $drawing->getWidth()); + self::assertSame(100, $drawing->getHeight()); + } + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/drawing_in_cell.xlsx b/tests/data/Reader/XLSX/drawing_in_cell.xlsx new file mode 100644 index 0000000000..a350d30581 Binary files /dev/null and b/tests/data/Reader/XLSX/drawing_in_cell.xlsx differ diff --git a/tests/data/Writer/XLSX/drawing_in_cell.xlsx b/tests/data/Writer/XLSX/drawing_in_cell.xlsx new file mode 100644 index 0000000000..a350d30581 Binary files /dev/null and b/tests/data/Writer/XLSX/drawing_in_cell.xlsx differ